Commit Graph

118 Commits

Author SHA1 Message Date
Jörg Lohrer 22997138f9 feat(app): i18n-init registriert messages und syncs mit activeLocale
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:33:19 +02:00
Jörg Lohrer 8f513495e3 feat(app): activeLocale-store mit persistence + initial-detection 2026-04-21 13:32:34 +02:00
Jörg Lohrer f799223836 chore(app): svelte-i18n + ui-messages-files (de/en)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:30:39 +02:00
Jörg Lohrer 5bab73def7 docs: plan 3/3 für multilinguale SPA (svelte-i18n + listen-filter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:29:38 +02:00
Jörg Lohrer 0fca9cbfa2 fix(app): post-route lädt reaktiv via $effect statt onMount
bei navigation zwischen slugs innerhalb der gleichen [...slug]-route
bleibt die komponente montiert — onMount feuert dann nicht mehr, und
der neue post lud erst nach manuellem reload. $effect auf dtag löst
das und rendert die neue view sofort.

race-condition-guard: currentDtag wird pro effect-lauf festgefroren;
stale responses werden verworfen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:17:41 +02:00
Jörg Lohrer bf7b52ab9b feat(content): erste englische übersetzung (bible-selfies) + bidirektionaler a-tag
- neue sprach-variante content/posts/en/bible-selfies/ verweist auf dt. original
- dt. bibel-selfies verweist zurück auf bible-selfies
- plan 2/3: npm test → npm run test:unit (existing script name)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:53:59 +02:00
Jörg Lohrer 4a06213d03 feat(app): LanguageAvailability-komponente in PostView eingebunden 2026-04-21 12:49:59 +02:00
Jörg Lohrer 7f48644dfc feat(app): loadTranslations liefert sprach-varianten eines posts 2026-04-21 12:43:52 +02:00
Jörg Lohrer 8f4125fcc9 feat(app): displayLanguage code→anzeigename 2026-04-21 12:41:29 +02:00
Jörg Lohrer ef20e13172 feat(app): parseTranslationRefs extrahiert a-tags mit marker translation 2026-04-21 12:37:08 +02:00
Jörg Lohrer c28a64ed49 docs: plan 2/3 für multilinguale SPA (a-tag-resolving + sprachhinweis)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:32:13 +02:00
Jörg Lohrer b9eb2c0bab test: re-trigger action nach contentRoot-pfad-fix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:18:51 +02:00
Jörg Lohrer ccbfc61a7c fix(publish): changedPostDirs gibt pfade mit original-contentRoot zurück 2026-04-21 10:18:20 +02:00
Jörg Lohrer 6055a8c1cc test: trigger action-smoketest nach contentRoot-fix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:12:22 +02:00
Jörg Lohrer b89442bf5c fix(publish): changedPostDirs normalisiert ../-präfix im contentRoot 2026-04-21 10:10:01 +02:00
Jörg Lohrer 367af9df9f test: trigger github-action nach struktur-migration
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:59:14 +02:00
Jörg Lohrer 00c4efb69a chore: posts nach content/posts/de/ migriert, lang+a-tag-platzhalter ergänzt 2026-04-21 09:51:22 +02:00
Jörg Lohrer c93befa925 feat(publish): buildKind30023 übernimmt a-tags aus frontmatter 2026-04-21 09:22:12 +02:00
Jörg Lohrer 1b0872a93f feat(publish): validatePost prüft a-tag-format 2026-04-21 09:19:35 +02:00
Jörg Lohrer 4986eae592 feat(publish): Frontmatter unterstützt a-tag-liste 2026-04-21 09:16:26 +02:00
Jörg Lohrer 66ff33e34a docs: plan 1/3 für multilinguale posts + spec-korrektur (26 statt 8 posts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:14:43 +02:00
Jörg Lohrer f977516552 test(publish): entferne toten index-mkdir im allPostDirs-fixture
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:14:16 +02:00
Jörg Lohrer 0c2e99dfeb feat(publish): allPostDirs traversiert sprach-ebene 2026-04-21 09:10:27 +02:00
Jörg Lohrer d3215fa760 feat(publish): filterPostDirs traversiert sprach-ebene 2026-04-21 09:06:43 +02:00
Jörg Lohrer 695f5e8e69 test: filterPostDirs für sprach-ebene (failing) 2026-04-21 09:05:53 +02:00
Jörg Lohrer 2b82994314 docs: design für multilinguale posts (content/posts/<lang>, l-/a-tags)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:48:29 +02:00
Jörg Lohrer 09fd7cb924 docs: status/handoff nach reimport aktualisiert
- Option A (repo/nostr-konflikt) + Option D (delete-subcommand) erledigt
- 26 statt 18 events, alltags-workflow explizit dokumentiert
- NIP-32 lang-tag als grundlage für spätere mehrsprachigkeit
- Slug-Hygiene-Stolperfalle ergänzt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:10:27 +02:00
Jörg Lohrer 7186c32067 feat: nostr-reimport von 8 client-posts + delete-subcommand + nip-32 lang-tag
8 neue Posts aus direkt-auf-Nostr-erstellten Events (Habla/Yakihonne) ins
Repo geholt, mit sauberen ASCII-slugs statt der kaputten d-tags (Umlaute,
Emojis, Doppelpunkte, Trailing-Dashes). Alte Events per NIP-09 geloescht.

Pipeline-Erweiterungen:
- neuer subcommand "delete" publisht NIP-09 kind:5 events via stabilem
  bunker-signer (nutzt CLIENT_SECRET_HEX-identitaet, keine re-pairings).
- frontmatter.lang + kind:30023 event tagt jetzt NIP-32 konform mit
  ["L","ISO-639-1"] + ["l","de","ISO-639-1"] (default: de).
- validate-post deno-task bekommt --allow-env (yaml-parser brauchts).

Vorbereitung fuer spaetere Mehrsprachigkeit: EN-Versionen koennen via
separate markdown-datei mit lang:en als eigenes event publiziert und
spaeter per a-tag-referenz zum DE-pendant verlinkt werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:50:37 +02:00
Jörg Lohrer 40785df346 docs: cutover dokumentiert, option A (repo/nostr-konflikt) priorisiert
- README: aktueller Live-Stand (SPA auf joerg-lohrer.de), CC0-Lizenz
- STATUS: Cutover 2026-04-18 erfasst, Staging/Prod beide auf joerglohrer26/
- HANDOFF: Option A als priorisierter nächster Schritt — 9 verwaiste
  Nostr-Events mit problematischen d-tags (Emojis, Doppelpunkte, Umlaute,
  leerer d-tag) brauchen Markdown-Reimport + saubere Slugs + NIP-09-Cleanup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:27:53 +02:00
Jörg Lohrer 54eb0b62cb feat: cc0-badge im footer + impressum auf cc0 umstellen
Footer zeigt jetzt CC0-Badge (Heart+Zero inline SVG, monochrom via
currentColor) statt "© Jörg Lohrer". Impressum entsprechend von
CC BY-SA auf CC0 umgestellt, mit freundlichem Hinweis, dass
Namensnennung erwünscht, aber nicht rechtlich erforderlich ist.

"Nostr-basiert" im Footer jetzt Link zum GitHub-Repo (Making-of).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:19:21 +02:00
Jörg Lohrer 10e455a078 spa: startseite + archiv + impressum + menü + assets für cutover
Startseite (+page.svelte) komplett überarbeitet:
  - Hero mit lokalem Profilbild (WebP aus static/, schneller als
    kind:0-roundtrip), Begrüßung "Hi Willkommen auf meinem Blog 🤗",
    About/Website aus kind:0
  - Social-Icons-Leiste (Nostr/Mastodon/Bluesky/LinkedIn/ORCID/Mail)
    als inline-SVG, monochrom via currentColor, hover färbt blau
  - Nostr-Icon von satscoffee/nostr_icons (outline, CC0), die anderen
    stilisiert als vereinfachte Brand-Icons
  - Neueste 5 Posts + Archiv-Link

Archiv-Route (/archiv/): alle Posts, nach Jahr gruppiert.

Impressum (/impressum/): static-page, rendert content/impressum.md
(via vite ?raw-import), bleibt aus nostr-feeds draußen. Frontmatter-
parser toleriert trailing-spaces auf --- zeilen.

Menü im Layout: sticky header mit brand + 3 links (Home, Archiv,
Impressum), aktiv-state via akzent-farbe. Footer mit © + Impressum
+ "Nostr-basiert"-hinweis.

Assets: profilbild und favicons aus dem hugo-static (repo-root) nach
app/static/ übernommen, favicon-links in app.html ergänzt.

NIP-05: .well-known/nostr.json in app/static angelegt mit CORS-header
via .htaccess, damit "joerglohrer@joerg-lohrer.de" nach cutover
verifizierbar bleibt.

E2E-Tests angepasst an neue hero/navigation-struktur, 29/29 unit + 4/4
e2e grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:35:05 +02:00
Jörg Lohrer 3f8d3e7592 docs: handoff/status aktualisiert — delete-event erledigt, design-todos offen
- nip-09-delete-event für duplikat 1744905463975 festgehalten
- open-todos ergänzt: kind:5-filter in spa, repo/nostr-konflikt-mgmt,
  delete-subcommand in pipeline
- option 3 (menü+impressum+startseite) als design-todo klargestellt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:30:37 +02:00
Jörg Lohrer 34c62cb944 spa/deploy: dynamische site-url via __SITE_URL__-platzhalter, staging + prod als deploy-targets
app.html nutzt __SITE_URL__ als platzhalter in og:url und canonical.
deploy-svelte.sh ersetzt ihn nach dem build pro ziel via sed:
  - svelte  → https://svelte.joerg-lohrer.de  (default, bisheriger SVELTE_FTP_-pfad)
  - staging → https://staging.joerg-lohrer.de (STAGING_FTP_-pfad, webroot joerglohrer26)
  - prod    → https://joerg-lohrer.de        (STAGING_FTP_-pfad, cutover-ziel)

env-auslese aus .env.local nicht mehr via `source` (bricht bei
sonderzeichen im passwort), sondern via awk pro schlüssel. build wird
jetzt vom deploy-skript angestoßen, damit immer gegen den frischen
html-stand gebaut wird.

app/.env.example dokumentiert PUBLIC_SITE_URL (derzeit ungenutzt, da
der platzhalter-ansatz zuverlässiger ist als runtime-env für prerender).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 10:01:08 +02:00
Jörg Lohrer 75ad8b87fa docs: alle 24 tasks der publish-pipeline abgeschlossen
spa → main gemergt, github-actions-workflow manuell verifiziert
(run #1: signer ok, outbox ok, blossom-liste ok, mode=diff posts=0).

der 24-task-plan aus docs/superpowers/plans/2026-04-16-publish-pipeline.md
ist offiziell durch. pipeline läuft sowohl lokal als auch in ci,
auto-trigger bei content-push ist konfiguriert (aber noch nicht real
ausgelöst worden — beiläufig mitzunehmen, wenn mal ein post editiert
wird).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 07:44:00 +02:00
Jörg Lohrer b2cbbb6390 docs: ci-setup-guide + status/handoff für ci-phase aktualisiert
docs/github-ci-setup.md dokumentiert:
  - forgejo → github push-mirror
  - die 4 github-repository-secrets (bunker-url, author-pubkey-hex,
    bootstrap-relay, client-secret-hex) — letzteres identisch mit
    .env.local für stabile amber-app-identität
  - wie man sie rotiert
  - migrations-pfad weg von github (woodpecker, cron)

status + handoff reflektieren: pipeline live, alle 18 posts publiziert,
91 bilder auf blossom, ci-setup steht, cutover als nächster schritt
möglich.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 07:27:48 +02:00
Jörg Lohrer 2f7f991bc6 publish(task 22): github-actions-workflow für auto-publish
.github/workflows/publish.yml triggert bei:
  - push auf main mit änderungen in content/posts/**
  - manuellem workflow_dispatch (optional mit force_all=true)

ablauf:
  1. deno 2.x setup
  2. pre-flight check (bunker, kind:10002, kind:10063)
  3. publish (diff-modus per default, force-all bei manuellem trigger)
  4. log-artefakt (publish-*.json, 30 tage retention)

benötigt 4 repo-secrets im github-ui:
  - BUNKER_URL
  - AUTHOR_PUBKEY_HEX
  - BOOTSTRAP_RELAY
  - CLIENT_SECRET_HEX (stabile client-identität für amber-permissions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 06:52:08 +02:00
Jörg Lohrer 0c6fdd15c3 publish(task 21): date-string-coercion + force-all migration erfolgreich
validatePost akzeptiert jetzt auch string-dates im YYYY-MM-DD- oder
ISO-8601-format und coerced sie in ein Date-objekt in-place. vorher
schlug die validation für 13 von 18 altposts fehl, weil deren yaml
`date: "2023-02-26"` quoted war (hugo-konvention) und der yaml-parser
strings statt Date-instanzen liefert.

migration durchgelaufen (log in docs/publish-logs/): 18/18 success,
91 bilder auf beiden blossom-servern, 5 write-relays — bis auf
relay.damus.io, der bei 6 posts nicht auf OK antwortet (üblich bei
damus, rate-limiting). alle 7 multi-relay-posts haben weiter mindestens
4 acks (über MIN_RELAY_ACKS=2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 06:48:27 +02:00
Jörg Lohrer db61149924 publish(tasks 19+20): signer-stabilisierung für wiederholte runs
probleme auf der realen amber-infrastruktur behoben:

1. ohne festen CLIENT_SECRET_HEX erzeugt applesauce bei jedem lauf einen
   neuen client-pubkey. amber bindet permissions pro client-pubkey, also
   sah jeder lauf wie eine neue unberechtigte app aus und bekam
   "no permission" als auto-antwort.
   → CLIENT_SECRET_HEX in config + cli, SimpleSigner.fromKey durchgereicht.

2. applesauce wirft bei "already connected"/"no permission" unhandled
   rejections, weil response-promises asynchron reagieren.
   → globaler unhandledrejection-handler, der diese benannten fehler
   schluckt; connect() im try/catch mit open+force als fallback.

3. timeout auf bunker connect auf 60s erhöht (amber-pairing kann
   menschliches tap dauern beim ersten mal).

einzel-post-publish live verifiziert:
- offenheit-das-wesentliche als kind:30023 publiziert
- alle 5 write-relays haben bestätigt
- bild auf beide blossom-server hochgeladen
- SPA rendert das bild von blossom.edufeed.org

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 06:42:28 +02:00
Jörg Lohrer 18d9dad56e publish(task 18): cli-entrypoint mit subcommand-dispatch
src/cli.ts dispatcht via @std/cli/parse-args:
  - publish [--force-all | --post <slug> | --dry-run]
  - check
  - validate-post <path>

cmdPublish orchestriert:
  1. config laden, signer initialisieren, outbox + blossom-server laden
  2. post-dirs resolven (diff/force-all/single per slug)
  3. dry-run → liste printen, exit 0
  4. für jeden post: processPost aufrufen, logger aktualisieren
  5. am ende: logs/publish-<iso>.json, exit-code je nach fehlern

resolvePostDirs schaltet zwischen den drei modi um und findet bei
--post <slug> den passenden ordner über allPostDirs + findBySlug.

smoke-tests aus dem plan (usage → exit 2, validate-post → ✓) gehen
durch. alle 57 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:45:54 +02:00
Jörg Lohrer 4d9af00a97 publish(task 17): validate-post-subcommand
validatePostFile(path) liest post-datei, parst frontmatter, ruft
validatePost aus core/validation. liefert { ok, slug?, error? }.
offline-befehl ohne netz-zugriff. 3 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:40:29 +02:00
Jörg Lohrer f31e586e85 publish(task 16): check-subcommand (pre-flight-validation)
runCheck(config) prüft vor jedem publish-run:
  - bunker-signer erreichbar und pubkey matcht AUTHOR_PUBKEY_HEX
  - kind:10002 outbox-liste enthält mindestens 1 write-relay
  - kind:10063 blossom-liste nicht leer, alle server antworten auf
    HEAD / (405 wird toleriert — blossom-server erlauben oft kein HEAD)

liefert { ok, issues }. printCheckResult() druckt ✓ oder ✗ + liste.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:39:51 +02:00
Jörg Lohrer 68ea912fad publish(task 15): processPost — kern-pipeline pro post (tdd)
processPost(args) orchestriert pro post:
  1. frontmatter parsen + validieren
  2. draft → skipped-draft
  3. bilder sammeln + sequentiell zu blossom hochladen (mapping
     dateiname → primary-url)
  4. body mit rewriteImageUrls anpassen, coverUrl via resolveCoverUrl
  5. kind:30023 event bauen via buildKind30023
  6. checkExisting → action = new|update
  7. signieren via nip-46
  8. publishToRelays, prüfen ob minRelayAcks erreicht

alle externen abhängigkeiten (readPostFile, collectImages, upload,
sign, publish, checkExisting) via PostDeps-interface eingezogen
— einfach mockbar. fehler aller art landen als { status: failed,
error: msg }. 6 tests grün.

follow-up (nicht teil von task 15): license-tag und imeta-tags aus
images[]-frontmatter sind noch nicht im event. kommt in eigenem
folge-task, basierend auf der metadata-convention-spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:37:05 +02:00
Jörg Lohrer db85061287 publish(task 14): structured json logger
createLogger(opts) sammelt postSuccess/postFailed/postSkippedDraft-
events, druckt menschenlesbare zeilen (✓/✗/-), liefert am ende ein
RunLog mit allen einträgen plus start/end-timestamps. writeJson()
schreibt die komplette summary als json für archivierung/ci-artifact.
2 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:35:41 +02:00
Jörg Lohrer b6196f1052 publish(task 13): git-diff change-detection für post-ordner
filterPostDirs(lines, contentRoot) extrahiert post-verzeichnisse aus
git-diff-ausgabe (index.md-matches + asset-changes), ignoriert
_drafts/. contentRoot ist parameter (blaupausen-tauglich für nicht-
hugo-layouts). changedPostDirs(from, to, contentRoot, runner?) ruft
"git diff --name-only A..B" via dependency-injected runner. Plus
allPostDirs() für den --force-all-modus. 4 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:33:56 +02:00
Jörg Lohrer 02a955c46f publish(task 12): blossom-upload mit multi-server, bud-01 auth
uploadBlob(args) berechnet sha256, baut kind:24242-auth-event via
signer, schickt es base64-kodiert im authorization-header an PUT
/upload aller servers parallel. erfolg: report mit ok/failed-listen
und primaryUrl (erster erfolgreicher server). wirft wenn alle ablehnen.
BlossomClient via dependency-injection für tests.
TS-casts für Uint8Array→BufferSource/BodyInit (deno-strict). 3 tests
grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:32:58 +02:00
Jörg Lohrer 8eebd29266 publish(task 11): image-collector (ignoriert hugo-derivate)
collectImages(postDir) scannt ordner nach png/jpg/jpeg/gif/webp/svg,
ignoriert hugo-resize-derivate (*_hu_<hex>.*), liest bytes und gibt
sortierte ImageFile[]-liste zurück. mimeFromExt() mapped extension
auf mime-type. deno.jsonc test-task um --allow-write erweitert (Deno.
makeTempDir+writeFile in tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:31:32 +02:00
Jörg Lohrer 05ba4e4ef9 publish(task 10): nip-46 bunker-signer-wrapper mit timeout
createBunkerSigner(bunkerUrl) nutzt NostrConnectSigner.fromBunkerURI
aus applesauce-signers 2.x (früher als Nip46Signer im plan bezeichnet;
klassenname hat sich geändert). subscription- und publishMethod werden
global am class-constructor an einen shared RelayPool gekoppelt.
getPublicKey und signEvent bekommen je 30s-timeout mit sauberem
clearTimeout via withTimeout-helper. signer.ts ist vollständig, aber
ohne eigene unit-tests — integration folgt über check-subcommand
(task 16).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:30:15 +02:00
Jörg Lohrer 0a858371bf publish(task 9): blossom-server-liste-loader (kind:10063)
parseBlossomServers(ev) extrahiert "server"-tag-urls, normalisiert
trailing-slash. loadBlossomServers(bootstrapRelay, pubkey) fragt
kind:10063 via applesauce-relay. 3 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:28:44 +02:00
Jörg Lohrer 1ec48ad1a9 publish(task 8): outbox-relay-loader (kind:10002 parser + fetcher)
parseOutbox(ev) interpretiert nip-65 r-tags: ohne marker → read+write,
"read"/"write"-marker → nur jeweiliges set. ignoriert fremde tag-namen.
loadOutbox(bootstrapRelay, pubkey) fragt kind:10002-event via
applesauce-relay 2.x und parst das ergebnis. 3 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:27:47 +02:00
Jörg Lohrer ebe73cbf46 publish(task 7): relay-pool-wrapper (publish + checkExisting)
publishToRelays(urls, ev, opts) publisht signiertes event parallel zu
allen relays, mit retries + exponential backoff + timeout pro versuch.
retour: { ok: string[], failed: string[] }. default-pool via
applesauce-relay 2.x (new RelayPool()); publishFn via dependency-
injection für tests. checkExisting(slug, pubkey, urls) fragt je relay
nach kind:30023 mit #d-filter ab — true wenn irgendeiner matcht.
timer-leaks vermieden per clearTimeout in publishOne + im mock-test.
3 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:27:12 +02:00