diff --git a/docs/superpowers/plans/2026-04-09-tag-search.md b/docs/superpowers/plans/2026-04-09-tag-search.md deleted file mode 100644 index e15788bd..00000000 --- a/docs/superpowers/plans/2026-04-09-tag-search.md +++ /dev/null @@ -1,387 +0,0 @@ -# Tag Entry Autocomplete Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make the category/tag input in the Indiekit post compose form searchable by wiring a native datalist autocomplete to a MongoDB-backed `q=category` API endpoint. - -**Architecture:** Two patch scripts following the project's marker pattern. Patch 1 overrides the Micropub `q=category` handler to query MongoDB `posts.distinct("properties.category")` instead of the empty config array. Patch 2 adds a `` and debounced fetch to the `` web component. - -**Tech Stack:** Node.js ESM patch scripts, MongoDB `distinct()`, browser `fetch` + ``, existing `queryConfig` utility. - ---- - -## File Map - -| File | Action | Purpose | -|---|---|---| -| `scripts/patch-micropub-category-from-posts.mjs` | **Create** | Backend patch — MongoDB distinct query for `q=category` | -| `scripts/patch-tag-input-autocomplete.mjs` | **Create** | Frontend patch — datalist autocomplete on tag input | -| `package.json` | **Modify** | Register both patches in `postinstall` and `serve` scripts | - -Patch targets (read-only reference, patched in-place at startup): -- `node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js` — `case "categories":` inserted before `default:` -- `node_modules/@indiekit/frontend/components/tag-input/index.js` — datalist + event listeners appended before `return tagInput` - ---- - -## Task 1: Write the backend patch script - -**Files:** -- Create: `scripts/patch-micropub-category-from-posts.mjs` - -- [ ] **Step 1: Create the patch script** - -```js -/** - * Patch: make q=category return distinct tags from MongoDB posts collection. - * - * In queryController, line 28 rewrites q === "category" → "categories" before - * the switch, so the new case label must be "categories" (not "category"). - * The default branch returns config.categories (always []) because - * publication.categories is not configured. This patch inserts a - * case "categories": before default: that queries the posts collection instead. - * - * postsCollection may be undefined (no MongoDB). The ternary provides a [] - * fallback consistent with the guard pattern used throughout query.js. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const MARKER = "// [patch] micropub-category-from-posts"; - -const candidates = [ - "node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js", - "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js", -]; - -const OLD_SNIPPET = ` break; - } - - default: { - // Query configuration value (can be filtered, limited and offset) - if (config[q]) {`; - -const NEW_SNIPPET = ` break; - } - - case "categories": { ${MARKER} - const cats = postsCollection - ? (await postsCollection.distinct("properties.category")).filter(Boolean).sort() - : []; - response.json({ - categories: queryConfig(cats, { filter: filter?.toLowerCase(), limit, offset }), - }); - break; - } - - default: { - // Query configuration value (can be filtered, limited and offset) - if (config[q]) {`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let totalPatched = 0; -let totalChecked = 0; - -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - totalChecked++; - - const source = await readFile(filePath, "utf8"); - if (source.includes(MARKER)) { - console.log(`[postinstall] patch-micropub-category-from-posts: already applied to ${filePath}`); - continue; - } - - if (!source.includes(OLD_SNIPPET)) { - console.warn(`[postinstall] patch-micropub-category-from-posts: snippet not found in ${filePath} (upstream changed?)`); - continue; - } - - const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); - await writeFile(filePath, updated, "utf8"); - console.log(`[postinstall] Applied patch-micropub-category-from-posts to ${filePath}`); - totalPatched++; -} - -if (totalChecked === 0) { - console.log("[postinstall] patch-micropub-category-from-posts: no target files found"); -} else if (totalPatched === 0) { - console.log("[postinstall] patch-micropub-category-from-posts: already up to date"); -} else { - console.log(`[postinstall] patch-micropub-category-from-posts: patched ${totalPatched} file(s)`); -} -``` - -- [ ] **Step 2: Run the patch script to verify it applies cleanly** - -```bash -node scripts/patch-micropub-category-from-posts.mjs -``` - -Expected output: `Applied patch-micropub-category-from-posts to node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js` - -If you see "snippet not found", the source file has changed upstream. Open `node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js`, find the `default:` block at the end of the switch, and update `OLD_SNIPPET` to match its actual content exactly (spaces, not tabs). - -- [ ] **Step 3: Verify the patch landed correctly** - -```bash -grep -A 10 "case .categories" node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js -``` - -Expected: the new `case "categories":` block appears before `default:`. - -- [ ] **Step 4: Commit** - -```bash -git add scripts/patch-micropub-category-from-posts.mjs -git commit -m "feat: patch q=category to return distinct tags from MongoDB posts" -``` - ---- - -## Task 2: Write the frontend patch script - -**Files:** -- Create: `scripts/patch-tag-input-autocomplete.mjs` - -- [ ] **Step 1: Create the patch script** - -```js -/** - * Patch: add datalist autocomplete to the web component. - * - * Fetches /micropub?q=category&filter= as the user types (debounced - * 300ms) and populates a native . Selecting a suggestion fills the - * text field; the user confirms with comma or blur as usual. - * - * Datalist is explicitly cleared in keydown(Comma) and blur handlers because - * the existing handlers clear the field via programmatic .value = "" which - * does NOT fire the input event (HTML Living Standard). - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const MARKER = "// [patch] tag-input-autocomplete"; - -const candidates = [ - "node_modules/@indiekit/frontend/components/tag-input/index.js", - "node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/tag-input/index.js", -]; - -const OLD_SNIPPET = ` // Capture any value in input not converted to tag (for example, by clicking - // outside component before pressing tab key) and add to list of tags. - $tagInputInput.addEventListener("blur", () => { - if ($tagInputInput.value) { - tagInput.addTag($tagInputInput.value, false); - $tagInputInput.value = ""; - } - }); - - return tagInput;`; - -const NEW_SNIPPET = ` // Capture any value in input not converted to tag (for example, by clicking - // outside component before pressing tab key) and add to list of tags. - $tagInputInput.addEventListener("blur", () => { - if ($tagInputInput.value) { - tagInput.addTag($tagInputInput.value, false); - $tagInputInput.value = ""; - } - }); - - // Autocomplete: wire datalist to Micropub q=category endpoint ${MARKER} - const _micropubHref = - document.querySelector('link[rel="micropub"]')?.href ?? "/micropub"; - const _datalistId = - "tag-suggestions-" + (this.$replacedInput?.getAttribute("name") ?? Math.random().toString(36).slice(2)); - const _$datalist = document.createElement("datalist"); - _$datalist.id = _datalistId; - this.appendChild(_$datalist); - $tagInputInput.setAttribute("list", _datalistId); - - let _autocompleteTimer; - $tagInputInput.addEventListener("input", () => { - clearTimeout(_autocompleteTimer); - const _val = $tagInputInput.value.trim(); - if (!_val) { _$datalist.replaceChildren(); return; } - _autocompleteTimer = setTimeout(async () => { - try { - const _res = await fetch( - \`\${_micropubHref}?q=category&filter=\${encodeURIComponent(_val)}\` - ); - if (!_res.ok) return; - const _data = await _res.json(); - _$datalist.replaceChildren( - ...(_data.categories ?? []).map((_cat) => { - const _opt = document.createElement("option"); - _opt.value = _cat; - return _opt; - }) - ); - } catch {} - }, 300); - }); - - // Clear datalist after tag confirmed via comma (programmatic clear doesn't fire input) - $tagInputInput.addEventListener("keydown", (_e) => { - if (_e.code === "Comma") _$datalist.replaceChildren(); - }); - - // Clear datalist after tag confirmed via blur - $tagInputInput.addEventListener("blur", () => { - _$datalist.replaceChildren(); - }); - - return tagInput;`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let totalPatched = 0; -let totalChecked = 0; - -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - totalChecked++; - - const source = await readFile(filePath, "utf8"); - if (source.includes(MARKER)) { - console.log(`[postinstall] patch-tag-input-autocomplete: already applied to ${filePath}`); - continue; - } - - if (!source.includes(OLD_SNIPPET)) { - console.warn(`[postinstall] patch-tag-input-autocomplete: snippet not found in ${filePath} (upstream changed?)`); - continue; - } - - const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); - await writeFile(filePath, updated, "utf8"); - console.log(`[postinstall] Applied patch-tag-input-autocomplete to ${filePath}`); - totalPatched++; -} - -if (totalChecked === 0) { - console.log("[postinstall] patch-tag-input-autocomplete: no target files found"); -} else if (totalPatched === 0) { - console.log("[postinstall] patch-tag-input-autocomplete: already up to date"); -} else { - console.log(`[postinstall] patch-tag-input-autocomplete: patched ${totalPatched} file(s)`); -} -``` - -- [ ] **Step 2: Run the patch script to verify it applies cleanly** - -```bash -node scripts/patch-tag-input-autocomplete.mjs -``` - -Expected output: `Applied patch-tag-input-autocomplete to node_modules/@indiekit/frontend/components/tag-input/index.js` - -If you see "snippet not found", open the target file and compare the actual closing of the `blur` handler and `return tagInput` — they must match `OLD_SNIPPET` byte-for-byte including spaces (not tabs). - -- [ ] **Step 3: Verify the patch landed correctly** - -```bash -grep -A 5 "tag-input-autocomplete" node_modules/@indiekit/frontend/components/tag-input/index.js -``` - -Expected: the datalist setup code appears before `return tagInput;`. - -- [ ] **Step 4: Commit** - -```bash -git add scripts/patch-tag-input-autocomplete.mjs -git commit -m "feat: patch tag-input component with datalist autocomplete" -``` - ---- - -## Task 3: Register both patches in package.json - -**Files:** -- Modify: `package.json` - -Both patches are safe for `postinstall` and `serve` — they are idempotent (marker check skips re-application). - -- [ ] **Step 1: Add both patches to the end of the `postinstall` script** - -In `package.json`, the `postinstall` script currently ends with: - -``` -node scripts/patch-ap-startup-gate-bypass.mjs -``` - -Append to the end of `postinstall` (after `patch-ap-startup-gate-bypass.mjs`): - -``` -&& node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs -``` - -- [ ] **Step 2: Add both patches to the `serve` script before the final node command** - -In the `serve` script, `patch-ap-startup-gate-bypass.mjs` appears near the **start** (after preflights), not the end. The script currently ends with: - -``` -node scripts/patch-ap-oauth-token-expiry-fix.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs -``` - -Change it so the two new patches come between `patch-ap-oauth-token-expiry-fix.mjs` and the final `node --require` invocation: - -``` -node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs -``` - -- [ ] **Step 3: Verify JSON is valid** - -```bash -node -e "JSON.parse(require('fs').readFileSync('package.json','utf8')); console.log('OK')" -``` - -Expected: `OK` - -- [ ] **Step 4: Commit** - -```bash -git add package.json -git commit -m "chore: register tag autocomplete patches in postinstall and serve" -``` - ---- - -## Task 4: End-to-end verification - -No automated test framework exists. Verify manually after `npm run serve`. - -- [ ] **Step 1: Confirm backend endpoint returns tags** - -```bash -# While the server is running, test with curl (replace token with a valid one, -# or just open this URL in the browser while logged into the admin UI): -curl -s "http://localhost:3000/micropub?q=category" | node -e "process.stdin|>require('stream/consumers').json()|>d=>console.log(JSON.stringify(d,null,2))" 2>/dev/null || \ - curl -s "http://localhost:3000/micropub?q=category" -``` - -Expected: `{ "categories": ["photography", "travel", ...] }` — actual tags from your posts, sorted alphabetically. - -- [ ] **Step 2: Confirm filter works** - -Open in browser (while logged in): -``` -http://localhost:3000/micropub?q=category&filter=photo -``` - -Expected: only tags containing "photo" (case-insensitive). - -- [ ] **Step 3: Verify autocomplete in the compose UI** - -1. Open `/posts/create` or the AP/Microsub compose form -2. Click the category/tag field -3. Type 2–3 characters of a known tag -4. Verify a dropdown appears with matching suggestions -5. Click a suggestion → text field fills with the tag name -6. Press comma → tag becomes a pill, dropdown clears -7. Type another tag partially → blur by clicking elsewhere → tag confirmed, dropdown clears diff --git a/docs/superpowers/plans/2026-04-12-ap-patch-consolidation.md b/docs/superpowers/plans/2026-04-12-ap-patch-consolidation.md deleted file mode 100644 index 27664515..00000000 --- a/docs/superpowers/plans/2026-04-12-ap-patch-consolidation.md +++ /dev/null @@ -1,486 +0,0 @@ -# AP Patch Consolidation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Consolidate 27 individual AP patch scripts into 6 concern-based scripts, removing 2 dead patches, to make `postinstall`/`serve` maintainable. - -**Architecture:** Each consolidated script reads its target file(s) once, applies multiple patches in sequence using a shared `applyPatch()` helper, and writes once per file. Per-patch MARKER idempotency is preserved. Ordering within a script matters when one patch's output is the next patch's OLD_SNIPPET anchor (documented per script). - -**Tech Stack:** Node.js ESM (`.mjs`), `node:fs/promises` — same as existing patch scripts. No new dependencies. - ---- - -## File Map - -**Create:** -- `scripts/patch-ap-syndication.mjs` — absorbs 4 patches on `syndicator.js` -- `scripts/patch-ap-federation-infra.mjs` — absorbs 5 patches + delete-fix Change A across `federation-bridge.js`, `federation-setup.js`, `index.js` -- `scripts/patch-ap-mastodon-statuses.mjs` — absorbs 5 patches on `statuses.js` -- `scripts/patch-ap-mastodon-accounts.mjs` — absorbs 2 patches on `resolve-account.js` only (the spec header mentions `accounts.js` but both absorbed patches target `resolve-account.js`; `accounts.js` interaction patch is in misc) -- `scripts/patch-ap-mastodon-notifications.mjs` — absorbs 1 patch on `notifications.js` -- `scripts/patch-ap-mastodon-misc.mjs` — absorbs 8 patches across compose, interactions, og-image, inbox handlers - -**Modify:** -- `package.json` — replace 27 AP patch invocations with 6 consolidated ones in both `postinstall` and `serve` - -**Delete:** -- `scripts/patch-ap-syndicate-dedup.mjs` -- `scripts/patch-ap-syndicate-skip-checkin.mjs` -- `scripts/patch-ap-syndicate-skip-draft.mjs` -- `scripts/patch-ap-syndicate-skip-unlisted.mjs` -- `scripts/patch-ap-federation-bridge-base-url.mjs` -- `scripts/patch-ap-signature-host-header.mjs` -- `scripts/patch-ap-inbox-delivery-debug.mjs` -- `scripts/patch-ap-inbox-publication-url.mjs` -- `scripts/patch-ap-webfinger-before-auth.mjs` -- `scripts/patch-ap-mastodon-reply-threading.mjs` -- `scripts/patch-ap-mastodon-status-id.mjs` -- `scripts/patch-ap-mastodon-delete-fix.mjs` -- `scripts/patch-ap-status-reply-id.mjs` -- `scripts/patch-ap-interactions-context-state.mjs` -- `scripts/patch-ap-actor-cache-await.mjs` -- `scripts/patch-ap-resolve-account-timeout-safe.mjs` -- `scripts/patch-ap-notifications-status-lookup.mjs` -- `scripts/patch-ap-compose-default-checked.mjs` -- `scripts/patch-ap-og-image.mjs` -- `scripts/patch-ap-repost-announce-fix.mjs` -- `scripts/patch-ap-interactions-send-guard.mjs` -- `scripts/patch-ap-interactions-cleanup-preserve.mjs` -- `scripts/patch-ap-interactions-accounts-uid.mjs` -- `scripts/patch-inbox-ignore-view-activity.mjs` -- `scripts/patch-inbox-skip-view-activity-parse.mjs` -- `scripts/patch-ap-account-lookup-cache-fallback.mjs` (dead — upstream fixed) -- `scripts/patch-ap-skip-draft-syndication.mjs` (dead — OLD_SNIPPET no longer matches) - ---- - -## Shared Helper Pattern - -Every consolidated script uses this structure. Copy it exactly — do not vary across scripts: - -```js -import { access, readFile, writeFile } from "node:fs/promises"; - -const AP_BASE = "@rmdes/indiekit-endpoint-activitypub"; -const AP_ROOTS = [ - `node_modules/${AP_BASE}`, - `node_modules/@indiekit/indiekit/node_modules/${AP_BASE}`, -]; - -function apPath(rel) { - return AP_ROOTS.map(r => `${r}/${rel}`); -} - -async function fileExists(p) { - try { await access(p); return true; } catch { return false; } -} - -/** - * Apply a single patch to a file. - * Returns: "applied" | "already_applied" | "snippet_not_found" | "file_not_found" - */ -async function applyPatch(filePath, marker, oldSnippet, newSnippet) { - if (!(await fileExists(filePath))) return "file_not_found"; - const src = await readFile(filePath, "utf8"); - if (src.includes(marker)) return "already_applied"; - if (!src.includes(oldSnippet)) return "snippet_not_found"; - await writeFile(filePath, src.replace(oldSnippet, newSnippet), "utf8"); - return "applied"; -} - -const SCRIPT = "patch-ap-"; // change per script - -const PATCHES = [ - // { name, files: string[], marker, oldSnippet, newSnippet } - // NOTE: Order matters when one patch's newSnippet is the next patch's oldSnippet anchor -]; - -let total = 0; -for (const p of PATCHES) { - let done = false; - for (const f of p.files) { - const r = await applyPatch(f, p.marker, p.oldSnippet, p.newSnippet); - if (r === "applied") { - console.log(`[postinstall] ${SCRIPT}: applied ${p.name} to ${f}`); - total++; done = true; break; - } else if (r === "already_applied") { - console.log(`[postinstall] ${SCRIPT}: ${p.name} already applied in ${f}`); - done = true; break; - } else if (r === "snippet_not_found") { - console.warn(`[postinstall] ${SCRIPT}: ${p.name} — snippet not found in ${f}, skipping`); - } - } - if (!done) console.log(`[postinstall] ${SCRIPT}: ${p.name} — no target file found`); -} -console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`); -``` - ---- - -## Task 1: Create `patch-ap-syndication.mjs` - -**Absorbs:** `patch-ap-syndicate-dedup`, `patch-ap-syndicate-skip-checkin`, `patch-ap-syndicate-skip-draft`, `patch-ap-syndicate-skip-unlisted` -**Target file:** `lib/syndicator.js` - -**ORDERING CONSTRAINT:** dedup → checkin → draft → unlisted. Each patch's `newSnippet` is the next patch's `oldSnippet` anchor. Wrong order = silent skip. - -- [ ] **Read all 4 source scripts** to extract MARKER / OLD_SNIPPET / NEW_SNIPPET for each: - - `scripts/patch-ap-syndicate-dedup.mjs` - - `scripts/patch-ap-syndicate-skip-checkin.mjs` - - `scripts/patch-ap-syndicate-skip-draft.mjs` - - `scripts/patch-ap-syndicate-skip-unlisted.mjs` - -- [ ] **Create `scripts/patch-ap-syndication.mjs`** using the shared helper pattern above. - - Set `SCRIPT = "patch-ap-syndication"` - - All 4 patches use `files: apPath("lib/syndicator.js")` - - List PATCHES in order: dedup, checkin, draft, unlisted - -- [ ] **Verify against local node_modules:** - ```bash - node scripts/patch-ap-syndication.mjs - ``` - Expected: 4 lines of `applied patch-ap-syndicate-*` (or `already applied` if patches were previously applied). - If you see `snippet not found` — the ordering is wrong or the source script's OLD_SNIPPET differs from what's in the file. Re-read the source scripts. - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-syndication.mjs - git commit -m "feat: consolidated patch-ap-syndication (dedup, checkin, draft, unlisted)" - ``` - ---- - -## Task 2: Create `patch-ap-federation-infra.mjs` - -**Absorbs:** `patch-ap-federation-bridge-base-url`, `patch-ap-signature-host-header`, `patch-ap-inbox-delivery-debug`, `patch-ap-inbox-publication-url`, `patch-ap-webfinger-before-auth`, and `patch-ap-mastodon-delete-fix` **Change A only** (the `index.js` portion that exposes `broadcastDelete`) -**Target files:** `lib/federation-bridge.js`, `lib/federation-setup.js`, `index.js` - -- [ ] **Read all 6 source scripts:** - - `scripts/patch-ap-federation-bridge-base-url.mjs` - - `scripts/patch-ap-signature-host-header.mjs` - - `scripts/patch-ap-inbox-delivery-debug.mjs` - - `scripts/patch-ap-inbox-publication-url.mjs` - - `scripts/patch-ap-webfinger-before-auth.mjs` - - `scripts/patch-ap-mastodon-delete-fix.mjs` — **Change A only** (the snippet targeting `index.js`; Changes B and C target `statuses.js` and go in Task 3) - -- [ ] **Note `federation-bridge.js` vs `federation-setup.js`:** - `patch-ap-inbox-delivery-debug` targets two files. Read the script carefully — it patches one snippet in `federation-bridge.js` and one in `federation-setup.js`. Both get their own PATCHES entry with their respective `apPath()` calls. - -- [ ] **Note `patch-ap-federation-bridge-base-url` multi-file/multi-snippet:** - This script patches multiple files and/or multiple snippets (e.g., one change in `federation-bridge.js` and one in `index.js`). Each distinct file+snippet combination needs its own PATCHES entry. Do not try to merge them into a single entry — the `applyPatch` helper handles one snippet per call. - -- [ ] **ORDERING CONSTRAINT within `federation-bridge.js` entries:** - `patch-ap-inbox-delivery-debug`'s Fix B (the `federation-bridge.js` snippet) anchors on a comment injected by `patch-ap-federation-bridge-base-url` (specifically, the `// ap-base-url patch` comment in the surrounding code). In the PATCHES array, the `federation-bridge-base-url` entry for `federation-bridge.js` must appear **before** the `inbox-delivery-debug` entry for `federation-bridge.js`. Wrong order → delivery-debug snippet not found → silent skip. - -- [ ] **Create `scripts/patch-ap-federation-infra.mjs`** using the shared helper. - - Set `SCRIPT = "patch-ap-federation-infra"` - - Patches targeting `index.js` use `files: apPath("index.js")` — note: `index.js` is at the module root, not under `lib/` - - Apply federation-bridge-base-url entries before inbox-delivery-debug entries in the PATCHES array - -- [ ] **Verify:** - ```bash - node scripts/patch-ap-federation-infra.mjs - ``` - Expected: one `applied` or `already applied` line per patch (6 patches across 3+ files). - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-federation-infra.mjs - git commit -m "feat: consolidated patch-ap-federation-infra (bridge, signatures, inbox, webfinger, delete-fix Change A)" - ``` - ---- - -## Task 3: Create `patch-ap-mastodon-statuses.mjs` - -**Absorbs:** `patch-ap-mastodon-reply-threading`, `patch-ap-mastodon-status-id`, `patch-ap-mastodon-delete-fix` **Changes B and C only**, `patch-ap-status-reply-id` **Change B only**, `patch-ap-interactions-context-state` -**Target file:** `lib/mastodon/routes/statuses.js` - -- [ ] **Read 5 source scripts:** - - `scripts/patch-ap-mastodon-reply-threading.mjs` - - `scripts/patch-ap-mastodon-status-id.mjs` - - `scripts/patch-ap-mastodon-delete-fix.mjs` — Changes B and C only (skip the `index.js` Change A already handled in Task 2) - - `scripts/patch-ap-status-reply-id.mjs` — **Change B only**; Change A is upstream-fixed (the script's own upstream-fix guard skips it — include only Change B's OLD/NEW_SNIPPET) - - `scripts/patch-ap-interactions-context-state.mjs` - -- [ ] **For `patch-ap-status-reply-id` Change A:** The script checks whether the upstream fix is already present. For the consolidated script, simply omit Change A's snippet entirely — if it was needed, it would already be part of the upstream source. - -- [ ] **Create `scripts/patch-ap-mastodon-statuses.mjs`** using the shared helper. - - Set `SCRIPT = "patch-ap-mastodon-statuses"` - - All patches: `files: apPath("lib/mastodon/routes/statuses.js")` - -- [ ] **Verify:** - ```bash - node scripts/patch-ap-mastodon-statuses.mjs - ``` - Expected: one line per patch (5 patches). - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-mastodon-statuses.mjs - git commit -m "feat: consolidated patch-ap-mastodon-statuses (threading, status-id, delete, reply-id, interactions)" - ``` - ---- - -## Task 4: Create `patch-ap-mastodon-accounts.mjs` - -**Absorbs:** `patch-ap-actor-cache-await`, `patch-ap-resolve-account-timeout-safe` -**Target files:** `lib/mastodon/helpers/resolve-account.js` -**Drops:** `patch-ap-account-lookup-cache-fallback` — upstream already has the fix; script unconditionally skips. - -- [ ] **Read 2 source scripts:** - - `scripts/patch-ap-actor-cache-await.mjs` - - `scripts/patch-ap-resolve-account-timeout-safe.mjs` - -- [ ] **Create `scripts/patch-ap-mastodon-accounts.mjs`** using the shared helper. - - Set `SCRIPT = "patch-ap-mastodon-accounts"` - - Both patches target `resolve-account.js`: `files: apPath("lib/mastodon/helpers/resolve-account.js")` - - No ordering dependency between the two patches - -- [ ] **Verify:** - ```bash - node scripts/patch-ap-mastodon-accounts.mjs - ``` - Expected: 2 lines (applied or already applied). - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-mastodon-accounts.mjs - git commit -m "feat: consolidated patch-ap-mastodon-accounts (actor-cache-await, timeout-safe)" - ``` - ---- - -## Task 5: Create `patch-ap-mastodon-notifications.mjs` - -**Absorbs:** `patch-ap-notifications-status-lookup` -**Target file:** `lib/mastodon/routes/notifications.js` - -- [ ] **Read source script:** `scripts/patch-ap-notifications-status-lookup.mjs` - -- [ ] **Create `scripts/patch-ap-mastodon-notifications.mjs`** using the shared helper. - - Set `SCRIPT = "patch-ap-mastodon-notifications"` - - One patch: `files: apPath("lib/mastodon/routes/notifications.js")` - -- [ ] **Verify:** - ```bash - node scripts/patch-ap-mastodon-notifications.mjs - ``` - Expected: 1 line (applied or already applied). - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-mastodon-notifications.mjs - git commit -m "feat: consolidated patch-ap-mastodon-notifications (status-lookup)" - ``` - ---- - -## Task 6: Create `patch-ap-mastodon-misc.mjs` - -**Absorbs:** `patch-ap-compose-default-checked`, `patch-ap-og-image`, `patch-ap-repost-announce-fix`, `patch-ap-interactions-send-guard`, `patch-ap-interactions-cleanup-preserve`, `patch-ap-interactions-accounts-uid`, `patch-inbox-ignore-view-activity`, `patch-inbox-skip-view-activity-parse` -**Target files:** various (compose template, og-image handler, interactions handler, inbox handlers) - -- [ ] **Read all 8 source scripts** and note their exact target file paths (some will be under `lib/mastodon/routes/`, some under `lib/` root level or `lib/ap/`): - - `scripts/patch-ap-compose-default-checked.mjs` - - `scripts/patch-ap-og-image.mjs` - - `scripts/patch-ap-repost-announce-fix.mjs` - - `scripts/patch-ap-interactions-send-guard.mjs` - - `scripts/patch-ap-interactions-cleanup-preserve.mjs` - - `scripts/patch-ap-interactions-accounts-uid.mjs` - - `scripts/patch-inbox-ignore-view-activity.mjs` - - `scripts/patch-inbox-skip-view-activity-parse.mjs` - -- [ ] **Confirm candidate paths per patch** — some of these scripts may target files outside `lib/mastodon/` (e.g., compose template may be in `templates/`, inbox handlers may be in `lib/ap/`). Use exactly the candidate paths from each source script. - -- [ ] **Create `scripts/patch-ap-mastodon-misc.mjs`** using the shared helper. - - Set `SCRIPT = "patch-ap-mastodon-misc"` - - Each patch uses its correct `files` array (not all will use `apPath()` — copy candidate arrays verbatim from source scripts) - - No ordering dependencies — all patches target independent snippets or different files - -- [ ] **Verify:** - ```bash - node scripts/patch-ap-mastodon-misc.mjs - ``` - Expected: 8 lines (applied or already applied). - -- [ ] **Commit:** - ```bash - git add scripts/patch-ap-mastodon-misc.mjs - git commit -m "feat: consolidated patch-ap-mastodon-misc (compose, og-image, interactions, inbox)" - ``` - ---- - -## Task 7: Update `package.json` - -Replace the 27 individual AP patch invocations in both `postinstall` and `serve` with the 6 consolidated scripts (+ 2 standalone). - -- [ ] **Open `package.json`** and locate both `postinstall` and `serve` strings. - -- [ ] **In `postinstall`:** Remove these 27 entries (all `node scripts/patch-ap-*.mjs` and `node scripts/patch-inbox-*.mjs` that were consolidated or are dead): - - `patch-ap-skip-draft-syndication` (dead — drop entirely) - - `patch-ap-og-image` - - `patch-ap-webfinger-before-auth` - - `patch-ap-federation-bridge-base-url` - - `patch-ap-signature-host-header` - - `patch-ap-compose-default-checked` - - `patch-ap-mastodon-reply-threading` - - `patch-ap-mastodon-status-id` - - `patch-ap-interactions-send-guard` - - `patch-ap-interactions-cleanup-preserve` - - `patch-ap-interactions-accounts-uid` - - `patch-ap-interactions-context-state` - - `patch-ap-syndicate-dedup` - - `patch-ap-syndicate-skip-checkin` - - `patch-ap-syndicate-skip-draft` - - `patch-ap-syndicate-skip-unlisted` - - `patch-ap-mastodon-delete-fix` - - `patch-ap-status-reply-id` - - `patch-ap-inbox-publication-url` - - `patch-ap-inbox-delivery-debug` - - `patch-ap-repost-announce-fix` - - `patch-inbox-ignore-view-activity` - - `patch-inbox-skip-view-activity-parse` - - `patch-ap-actor-cache-await` - - `patch-ap-resolve-account-timeout-safe` - - `patch-ap-account-lookup-cache-fallback` (dead — drop entirely) - - `patch-ap-notifications-status-lookup` - - The 27 removed entries currently span from `patch-ap-skip-draft-syndication` through `patch-ap-notifications-status-lookup`, with `patch-ap-startup-gate-bypass` interleaved near the end of that block (just before `patch-micropub-category-from-posts`). When deleting the block, keep `patch-ap-startup-gate-bypass` in place — remove everything around it, not the entry itself. - - After removal, the `patch-ap-startup-gate-bypass` entry stays at its current position in `postinstall`. Insert the 6 consolidated scripts immediately after it, before `patch-micropub-category-from-posts`: - ``` - node scripts/patch-ap-startup-gate-bypass.mjs && - node scripts/patch-ap-oauth-token-expiry-fix.mjs && - node scripts/patch-ap-federation-infra.mjs && - node scripts/patch-ap-syndication.mjs && - node scripts/patch-ap-mastodon-statuses.mjs && - node scripts/patch-ap-mastodon-accounts.mjs && - node scripts/patch-ap-mastodon-notifications.mjs && - node scripts/patch-ap-mastodon-misc.mjs && - node scripts/patch-micropub-category-from-posts.mjs && ... - ``` - -- [ ] **In `serve`:** Apply the same replacement logic. `patch-ap-startup-gate-bypass` must remain **first** in `serve` (it already is the first entry in the serve script — do not move it). - -- [ ] **Verify the JSON is valid:** - ```bash - node -e "require('./package.json')" && echo "valid" - ``` - Expected: `valid` - -- [ ] **Dry-run the full postinstall sequence** (without actually running npm install — just run the consolidated scripts manually in order): - ```bash - node scripts/patch-ap-federation-infra.mjs && \ - node scripts/patch-ap-syndication.mjs && \ - node scripts/patch-ap-mastodon-statuses.mjs && \ - node scripts/patch-ap-mastodon-accounts.mjs && \ - node scripts/patch-ap-mastodon-notifications.mjs && \ - node scripts/patch-ap-mastodon-misc.mjs - ``` - Expected: all report `already applied` (since Tasks 1-6 already applied them). No `snippet not found` warnings. - -- [ ] **Commit:** - ```bash - git add package.json - git commit -m "refactor: replace 27 individual AP patches with 6 consolidated scripts in postinstall/serve" - ``` - ---- - -## Task 8: Delete old scripts - -- [ ] **Delete the 27 consolidated/dead source scripts:** - ```bash - cd "/Users/sven/Library/Mobile Documents/com~apple~CloudDocs/PARA/1. Projects/indiekit-server" - git rm \ - scripts/patch-ap-skip-draft-syndication.mjs \ - scripts/patch-ap-og-image.mjs \ - scripts/patch-ap-webfinger-before-auth.mjs \ - scripts/patch-ap-federation-bridge-base-url.mjs \ - scripts/patch-ap-signature-host-header.mjs \ - scripts/patch-ap-compose-default-checked.mjs \ - scripts/patch-ap-mastodon-reply-threading.mjs \ - scripts/patch-ap-mastodon-status-id.mjs \ - scripts/patch-ap-interactions-send-guard.mjs \ - scripts/patch-ap-interactions-cleanup-preserve.mjs \ - scripts/patch-ap-interactions-accounts-uid.mjs \ - scripts/patch-ap-interactions-context-state.mjs \ - scripts/patch-ap-syndicate-dedup.mjs \ - scripts/patch-ap-syndicate-skip-checkin.mjs \ - scripts/patch-ap-syndicate-skip-draft.mjs \ - scripts/patch-ap-syndicate-skip-unlisted.mjs \ - scripts/patch-ap-mastodon-delete-fix.mjs \ - scripts/patch-ap-status-reply-id.mjs \ - scripts/patch-ap-inbox-publication-url.mjs \ - scripts/patch-ap-inbox-delivery-debug.mjs \ - scripts/patch-ap-repost-announce-fix.mjs \ - scripts/patch-inbox-ignore-view-activity.mjs \ - scripts/patch-inbox-skip-view-activity-parse.mjs \ - scripts/patch-ap-actor-cache-await.mjs \ - scripts/patch-ap-resolve-account-timeout-safe.mjs \ - scripts/patch-ap-account-lookup-cache-fallback.mjs \ - scripts/patch-ap-notifications-status-lookup.mjs - ``` - -- [ ] **Verify no references remain in package.json:** - ```bash - grep -c "patch-ap-syndicate-dedup\|patch-ap-account-lookup-cache-fallback\|patch-ap-skip-draft-syndication" package.json - ``` - Expected: `0` - -- [ ] **Commit:** - ```bash - git commit -m "refactor: delete 27 consolidated/dead AP patch scripts" - ``` - ---- - -## Task 9: Final verification - -- [ ] **Count remaining AP patch scripts:** - ```bash - ls scripts/patch-ap-*.mjs scripts/patch-inbox-*.mjs 2>/dev/null - ``` - Expected: exactly these files remain: - - `patch-ap-startup-gate-bypass.mjs` - - `patch-ap-oauth-token-expiry-fix.mjs` - - `patch-ap-syndication.mjs` - - `patch-ap-federation-infra.mjs` - - `patch-ap-mastodon-statuses.mjs` - - `patch-ap-mastodon-accounts.mjs` - - `patch-ap-mastodon-notifications.mjs` - - `patch-ap-mastodon-misc.mjs` - -- [ ] **Simulate a clean postinstall:** - ```bash - npm run postinstall 2>&1 | grep -E "(patch-ap|patch-inbox|snippet not found|ERROR)" - ``` - If `node_modules` is already patched, all scripts report `already applied` — that's fine and expected. No `snippet not found`, no errors. - -- [ ] **Deep verify — confirm MARKERs are present in actual node_modules files** (this catches cases where a patch silently skipped on first run): - ```bash - # Syndicator: expect 4 markers - grep -c '\[patch\] ap-syndicate' node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js - # statuses.js: expect at least 4 markers - grep -c '\[patch\] ap-' node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js - # resolve-account.js: expect 2 markers - grep -c '\[patch\] ap-' node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/resolve-account.js - # notifications.js: expect 1 marker - grep -c '\[patch\] ap-' node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/notifications.js - ``` - If any count is 0 and the patch should have been applied, re-run the consolidated script directly and check for `snippet not found` warnings. - -- [ ] **Check package.json postinstall line length** (long lines can cause shell issues on some systems): - ```bash - node -e "const p = require('./package.json'); console.log('postinstall:', p.scripts.postinstall.length, 'chars'); console.log('serve:', p.scripts.serve.length, 'chars')" - ``` - Both should be noticeably shorter than before (was ~3000 chars each). - -- [ ] **Tag and push:** - ```bash - git log --oneline -8 - git push - ``` diff --git a/docs/superpowers/specs/2026-04-09-tag-search-design.md b/docs/superpowers/specs/2026-04-09-tag-search-design.md deleted file mode 100644 index 04af3775..00000000 --- a/docs/superpowers/specs/2026-04-09-tag-search-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Tag Entry Autocomplete — Design Spec - -**Date:** 2026-04-09 -**Status:** Approved - -## Problem - -The tag/category input in the Indiekit post compose form is a plain text field with no suggestions. The `q=category` Micropub endpoint returns an empty array because `publication.categories` is not configured — tags only exist in the `posts` MongoDB collection as `properties.category` values. - -## Solution - -Two patch scripts that wire up autocomplete end-to-end with no new endpoints and no changes to the Indiekit config. - ---- - -## Patch 1 — Backend: `patch-micropub-category-from-posts.mjs` - -**Target:** `node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js` - -**What it does:** - -Inserts a `case "categories":` block before the existing `default:` case in `queryController`. When `q=category` is requested, instead of falling through to `config.categories` (which is an empty array — truthy, so the `default:` branch does not throw, it just returns `{categories:[]}`), this case: - -1. Calls `postsCollection.distinct("properties.category")` to get every unique tag used across all published posts. MongoDB `distinct()` on an array field correctly unwraps array values, so `["photography","travel"]` per-post becomes individual distinct entries. -2. Filters out falsy values (`Boolean` filter) and sorts alphabetically. -3. Passes the result through `queryConfig(cats, { filter: filter?.toLowerCase(), limit, offset })` — note: `filter` is lowercased before passing to match `queryConfig`'s internal lowercase comparison, making search case-insensitive end-to-end. -4. Returns `{ categories: [...] }`. - -If `postsCollection` is not available, returns `{ categories: [] }` gracefully. - -**Example request/response:** -``` -GET /micropub?q=category&filter=pho -→ { "categories": ["photography"] } -``` - -**OLD_SNIPPET anchor:** The patch uses the closing `break;` of the preceding `source:` case plus the `default: {` line as context for the OLD_SNIPPET, since the `default:` block itself does not contain error-throwing logic for the `categories` query (empty array is truthy). - ---- - -## Patch 2 — Frontend: `patch-tag-input-autocomplete.mjs` - -**Target:** `node_modules/@indiekit/frontend/components/tag-input/index.js` - -**Micropub endpoint URL discovery:** - -The patch reads the Micropub endpoint URL using the standard IndieWeb discovery selector: - -```js -const micropubHref = document.querySelector('link[rel="micropub"]')?.href ?? "/micropub"; -``` - -This is resolved once per component instantiation. On admin pages Indiekit does not inject the `` element, so the fallback `/micropub` is always used in practice — which is correct for this deployment. - -**What it does:** - -After the `$tagInputInput` reference is obtained inside `TagInputFieldComponent.connectedCallback()`, appends the following behaviour: - -1. Creates a `` element (uid from the input's `name` attribute for stability) and appends it to the component. -2. Sets `$tagInputInput.setAttribute("list", datalistId)` — native browser datalist autocomplete applies to the text field. -3. Adds a debounced (300ms) `input` event listener: if the current value is ≥ 1 char, fetches `GET ?q=category&filter=` and replaces datalist `