From f58957937802d7ef056aeeab5b9e7bc406af2bab Mon Sep 17 00:00:00 2001
From: Silberengel
Date: Sat, 25 Apr 2026 12:08:19 +0200
Subject: [PATCH] bug-fixes
---
README.md | 47 +++++++++++++------
.../controllers/comment_reply_controller.js | 10 +++-
src/Command/PrewarmCommand.php | 4 +-
3 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index bc92d68..c667f42 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,19 @@
-A Symfony + FrankenPHP site that **reads Nostr long-form articles (kind 30023)** and related data from relays, stores articles in **MySQL**, and serves pages with Twig. **Comments and profile metadata** are **cache-backed** (not the full source of truth in the DB).
+A Symfony + FrankenPHP site that **reads Nostr long-form articles (kind 30023)** and related data from relays, and serves pages with Twig.
+
+### Where data lives
+
+| Data | Storage |
+|------|---------|
+| Published articles (30023/24) | **MySQL** `article` table (from `articles:get` / relay sync) |
+| Magazine index (30040), kind-0 **profiles**, NIP-65 **relay lists** (10002) | **MySQL** `event` table with stable `core_row_key` (filled by `app:prewarm` and on-demand fetches) |
+| Comment / reply / thread **UI** (fetched thread HTML, etc.) | **Filesystem cache** pool `cache.replies` (not the DB) |
+| Unpublished **editor preview** payloads | **Filesystem cache** pool `cache.drafts` |
+| Generic Symfony `cache.app` | Other app caches; **not** used for long-term profile or magazine index storage |
+
+NIP-09 kind-5 deletions that target stored kinds are applied to **MySQL** (articles + `event` rows). Relays are expected to handle ephemeral thread data.
---
@@ -40,9 +52,9 @@ A Symfony + FrankenPHP site that **reads Nostr long-form articles (kind 30023)**
---
-## Backfill articles + warm caches (recommended)
+## Backfill articles + prewarm (recommended)
-To **migrate**, **import articles from Nostr** for a time window, then **prewarm** magazine indices, author metadata, and comment caches:
+To **migrate**, **import articles from Nostr** for a time window, then run **prewarm** (magazine + profiles + deletions + comment cache):
```bash
make prewarm
@@ -51,20 +63,22 @@ make prewarm
| Step (script order) | Command / effect |
|---------------------|------------------|
| 1 | `docker compose up -d --wait` — starts **php**, **database**, and **cron** (the `cron` image runs a full `app:prewarm` on a 10 min schedule) |
-| 2 | `doctrine:migrations:migrate` |
-| 3 | `articles:get -- '-2 month' 'now'` — sync long-form into MySQL for that window |
-| 4 | `app:prewarm` — magazine **30040**, **kind-0** profiles, **comment** cache (default **`--comments-max=10`**, newest by `createdAt`) |
+| 2 | `doctrine:migrations:migrate` — applies schema (including `event` columns for core Nostr rows) |
+| 3 | `articles:get -- '-2 month' 'now'` — sync long-form into the `article` table |
+| 4 | `app:prewarm` — NIP-09 kind-5 sync (for stored kinds), magazine **30040** → `event`, kind-0 **profiles** (and relay lists on demand) → `event`, **comment** thread cache → `cache.replies` (default **`--comments-max=10`**, newest by `createdAt`) |
`make prewarm` brings the stack (including `cron`) up so scheduled prewarm is active. **Optional** extra arguments for the **cron**-scheduled `app:prewarm` go in **`.env`** as **`PREWARM_FLAGS`** (same as you might pass to `php bin/console app:prewarm …`); Compose passes them into the `cron` container. Example: `PREWARM_FLAGS="--metadata-limit=50 --no-magazine"`. **Restart** the `cron` service after changing `PREWARM_FLAGS` so the container reloads the env. On the **hub** stack, the `prewarm` service reads the same `PREWARM_FLAGS`; use `docker compose -f compose.hub.yaml up -d --force-recreate prewarm` after changing it.
+**Fresh database or major upgrade:** after schema changes, run **`articles:get`** + **`app:prewarm`** (or `make prewarm`) so `article` and `event` are repopulated from relays. There is no automatic migration of old PSR **profile** cache into MySQL.
+
---
## Console commands (overview)
| Command | Purpose |
|---------|---------|
-| `articles:get ` | Pull long-form articles from Nostr for the time range, persist to DB |
-| `app:prewarm` | Magazine relay refresh + metadata cache + comment cache warm |
+| `articles:get ` | Pull long-form articles from Nostr for the time range, persist to `article` |
+| `app:prewarm` | Magazine 30040 + kind-0 profile prewarm (→ `event`), NIP-09 deletions, comment thread warm (→ `cache.replies`) |
| `doctrine:migrations:migrate` | Apply SQL migrations |
| `user:elevate` | (If used) user elevation helper |
@@ -74,14 +88,16 @@ make prewarm
| Option | Default | Meaning |
|--------|---------|--------|
-| `--no-magazine` | off | Skip magazine 30040 index |
-| `--no-metadata` | off | Skip Nostr kind-0 / profile cache |
-| `--no-comments` | off | Skip comment thread cache |
-| `--metadata-limit` | `0` (all authors) | Cap distinct author pubkeys |
-| `--metadata-batch` | `50` | Pubkeys per batched Nostr `REQ` |
+| `--no-magazine` | off | Skip magazine 30040 index fetch / `event` update |
+| `--no-metadata` | off | Skip batched kind-0 profile prewarm (writes to `event`) |
+| `--no-deletions` | off | Skip NIP-09 kind-5 fetch and application (articles + `event` index/profile rows) |
+| `--deletion-since` | `-2 month` | `strtotime()` lower bound for kind-5 author-scoped fetch |
+| `--no-comments` | off | Skip comment thread prewarm (`cache.replies`) |
+| `--metadata-limit` | `0` (all authors) | Max distinct author pubkeys for the metadata phase |
+| `--metadata-batch` | `50` | Pubkeys per batched kind-0 Nostr `REQ` |
| `--comments-max` | `10` | Newest **N** articles (by `createdAt` **DESC**); `0` = all (still bounded by budget) |
| `--comments-budget` | `600` | Max wall seconds for the whole comments phase (Nostr is slow; raise e.g. `1200` if you need more articles in one run) |
-| `--magazine-budget` | `90` | Max wall seconds for magazine root + per-category 30040 fetches (hard-capped at 600s in code). If you have many categories, a **low** budget can stop before the last slug is refreshed—**stale home/category pages** until the next run. Set `MAGAZINE_PREWARM_PREFER_SLUGS` (comma-separated category `#d` slugs) to fetch those first after the root. |
+| `--magazine-budget` | `90` | Max wall seconds for magazine **per-category** 30040 fetches (root is separate; cap 600s in code). If you have many categories, a **low** budget can stop before the last slug is refreshed. Set `MAGAZINE_PREWARM_PREFER_SLUGS` (comma-separated category `#d` slugs) to fetch those first after the root. |
Prewarm clears the PHP **CLI** execution time limit for that run; relay work can be slow.
@@ -102,7 +118,8 @@ For a full **Nostr backfill** + one-shot prewarm, use **`make prewarm`** (or a h
| Site title, `npub`, `d_tag`, **relays** (`default_relay`, `article_relays`, `profile_relays`), theme | `config/unfold.yaml` (imported as Symfony parameters) |
| `MAGAZINE_PREWARM_PREFER_SLUGS` | `.env` / `.env.local` — optional comma-separated category slugs to prioritize in `app:prewarm` magazine phase (after the root). Use when the relay time budget would otherwise skip your updated category. |
| `DATABASE_URL`, `APP_SECRET`, `HTTP_PORT`, `MYSQL_*`, optional **`PREWARM_FLAGS`** (for the Docker `cron` service) | `.env` / `.env.local` (see `.env.dist`) |
-| Service wiring (e.g. cache, `NostrClient` args) | `config/services.yaml` |
+| Cache pool definitions (`cache.replies`, `cache.drafts`, `cache.app`) | `config/packages/cache.yaml` |
+| Service wiring (e.g. which pool comment loaders use) | `config/services.yaml` |
**Relays (short):** `default_relay` and `article_relays` drive article sync and many queries; `profile_relays` are used **first** for kind-0 / profile fetches, then the merged default + article set (see `NostrClient`).
diff --git a/assets/controllers/comment_reply_controller.js b/assets/controllers/comment_reply_controller.js
index 91bd886..edc63a4 100644
--- a/assets/controllers/comment_reply_controller.js
+++ b/assets/controllers/comment_reply_controller.js
@@ -163,6 +163,10 @@ export default class extends Controller {
* Reload the section HTML from the article comments fragment. After publishing, relays can lag;
* if `expectedEventIdHex` is set, re-fetch with backoff until the new note appears (or a cap is hit).
*
+ * Only sets `innerHTML` once the response actually contains the new `data-event-id`. Assigning on
+ * every poll replaced the whole subtree each time and re-instantiated every Stimulus `comment-reply`
+ * (connect/disconnect storms) while relays were still behind.
+ *
* @param {string} [expectedEventIdHex] lowercase 64-char hex
*/
async refreshThread(expectedEventIdHex = '') {
@@ -197,11 +201,13 @@ export default class extends Controller {
throw new Error(String(res.status));
}
const html = await res.text();
- container.innerHTML = html;
if (!wantId) {
+ container.innerHTML = html;
return;
}
- if (container.querySelector(`[data-event-id="${wantId}"]`)) {
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
+ if (parsed.querySelector(`[data-event-id="${wantId}"]`)) {
+ container.innerHTML = html;
return;
}
} catch {
diff --git a/src/Command/PrewarmCommand.php b/src/Command/PrewarmCommand.php
index ac31e93..481f8e2 100644
--- a/src/Command/PrewarmCommand.php
+++ b/src/Command/PrewarmCommand.php
@@ -61,9 +61,9 @@ final class PrewarmCommand extends Command
{
$this
->addOption('no-magazine', null, InputOption::VALUE_NONE, 'Skip magazine 30040 index fetch')
- ->addOption('no-deletions', null, InputOption::VALUE_NONE, 'Skip NIP-09 kind 5 deletion sync (30023/30024 DB + 30040 magazine cache)')
+ ->addOption('no-deletions', null, InputOption::VALUE_NONE, 'Skip NIP-09 kind 5 deletion sync (articles + event rows for stored kinds)')
->addOption('deletion-since', null, InputOption::VALUE_REQUIRED, 'strtotime() window start for kind 5 fetch', '-2 month')
- ->addOption('no-metadata', null, InputOption::VALUE_NONE, 'Skip Nostr profile metadata cache')
+ ->addOption('no-metadata', null, InputOption::VALUE_NONE, 'Skip batched kind-0 profile prewarm (MySQL event table)')
->addOption('no-comments', null, InputOption::VALUE_NONE, 'Skip comment thread cache')
->addOption('magazine-budget', null, InputOption::VALUE_REQUIRED, 'Seconds wall time for the category 30040 phase only (root fetch is not counted; capped at 600s). If many slugs, raise this or set MAGAZINE_PREWARM_PREFER_SLUGS', '90')
->addOption('metadata-limit', null, InputOption::VALUE_REQUIRED, 'Max distinct author pubkeys to warm (0 = all)', '0')