fix(admin): don't let one unreadable pad empty the Manage-pads list (#7935)#7938
Conversation
…7935) The admin /settings `padLoad` handler hydrates every pad via getPad() to build the listing (the default lastEdited sort forces a full scan). findKeys('pad:*','*:*:*') returns every key under the `pad:` prefix, including legacy/foreign or migration-corrupted records — e.g. a value stored as a JSON string rather than a pad object, which a botched dirty.db -> PostgreSQL migration produces. Loading such a record makes Pad.init throw `Cannot use 'in' operator to search for 'pool'`. The handler had no try/catch, so one bad record rejected the whole request: no `results:padLoad` was emitted (the SPA showed an empty "No results" state forever — the reported symptom) and the unhandled rejection could exit the server. Make loadMeta resilient — a failed getPad() logs a warning and returns zeroed metadata so the bad pad still surfaces for deletion instead of hiding every other pad — and wrap the handler so it always emits a terminal reply and never bubbles to the process-level handler. Tests: - backend: tests/backend/specs/admin/padLoadResilience.ts asserts a corrupt pad:* record no longer hides the readable pads (fails without the fix: no results:padLoad reply + server exit). - e2e: tests/frontend-new/admin-spec/admin_pads_page.spec.ts covers create pad -> welcome-page recent-pads -> /admin Manage-pads UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Code Review by Qodo
1.
|
PR Summary by QodoFix admin pad listing when a single pad record is unreadable WalkthroughsDescription• Make /settings padLoad resilient to unreadable pad:* records and always reply • Surface corrupt pads with zeroed metadata so admins can delete them • Add backend + Playwright regression coverage for Manage pads listing Diagramgraph TD
A["Admin Manage pads UI"] --> B["/settings: padLoad handler"] --> C["padManager.listAllPads()"] --> D{"Needs full scan?"}
D -->|"No"| G["emit results:padLoad (always)"]
D -->|"Yes"| E["loadMeta() per pad"] --> F["getPad() (try/catch)"] --> G
B -.-> H["log warn/error"]
F -.-> H
High-Level AssessmentThe following are alternative approaches to this PR: 1. Validate/clean pad:* keys at storage layer (DB/ueberdb2)
2. Pre-filter candidate pads using the pad index only (avoid findKeys fallout)
3. Background integrity check + admin cleanup UX
Recommendation: Keep the PR’s approach: defensive isolation in loadMeta() plus a top-level try/catch that always emits results:padLoad. It directly fixes the user-facing hang and prevents process-level unhandled rejections with minimal scope. Consider a follow-up integrity/cleanup mechanism if corrupted pad:* records are common, but the runtime guard should remain regardless. File ChangesBug fix (1)
Tests (2)
|
Qodo review: the error logs (and the error field returned to the SPA)
used `${err}` / err.message verbatim. For the corruption case the
TypeError message embeds the raw stored value ("...'pool' in <value>"),
which for a real pad can be document text — so logging it verbatim could
leak content, bloat logs, and let embedded newlines forge log lines.
Add a safeErr() helper (error name + single-line, 120-char-capped
message, control chars stripped) and use it in both the per-pad warning
and the outer handler error log, and for the error field emitted to the
client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Qodo review: padLoad now surfaces unreadable pad:* records (zeroed
metadata) so admins can delete them, but deletePad's normal path
(doesPadExists -> getPad -> Pad.remove) can't touch such a record:
doesPadExists() returns false for a non-object value and getPad() throws,
so the surfaced pad was effectively undeletable and the handler emitted
nothing.
When doesPadExists() is false but a raw `pad:${id}` value is present,
fall back to a raw key purge: sweep sub-keys (revs/chat/deletionToken/…)
via findKeys, drop the readonly mapping, then padManager.removePad()
(which removes the main key + pad-list + cache entry). Always emit a
terminal results:deletePad reply (including for an already-absent id) so
the UI clears the row instead of silently doing nothing, and wrap the
handler so failures are logged (sanitized) rather than swallowed.
Adds a backend test asserting a surfaced corrupt pad can be deleted and
disappears from the listing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @qodo — both findings actioned: 1. Undeletable corrupt pads ( 2. Logging the raw corrupt value ( All admin backend specs pass (8: 5 existing + 3 new) and |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
The delete test probed `db.get('pad:<id>')` for null right after deletePad.
That passed on postgres but failed on the dirty backend (Windows CI):
ueberdb2's per-backend read/write buffering can return the just-removed
value immediately after remove(), so it asserted storage internals rather
than behaviour. The deletion is still durable (same db.remove() every pad
uses; the in-memory pad-list entry is dropped synchronously). Assert the
behavioural outcome instead — the corrupt pad disappears from the padLoad
listing while the good pad remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #7935.
Symptom
Pads exist (visible on the welcome page, returned by the
listAllPadsAPI) but the admin Manage pads UI shows none. Reported on 3.3.0 with Docker + Traefik + PostgreSQL 18.Root cause
The welcome-page "recent pads" list is browser localStorage, so it reflects pads this browser opened — not the DB. The admin list comes from the
/settingssocketpadLoadhandler →listAllPads()→findKeys('pad:*', '*:*:*').The admin's default
lastEditedsort forces a full scan, hydrating every pad viagetPad().findKeys('pad:*','*:*:*')returns everypad:-prefixed key, including legacy/foreign/migration-corrupted records. A record whose stored value is a non-object (a JSON string — exactly what a botched dirty.db→Postgres migration produces) makesPad.initthrowCannot use 'in' operator to search for 'pool'(Pad.ts:591).The handler had no try/catch, so a single bad record:
server.ts→process.exit— taking the whole server down.Verified against a real PostgreSQL 18.4 instance: ueberdb2 6.1.8
findKeys,listAllPads, andgetStatsall work correctly there. PG18 is not the cause — the trigger is one unreadablepad:*record, which can happen on any DB backend.Fix
loadMetawrapsgetPad()in try/catch: a failed read logs a warning and returns zeroed metadata, so the bad pad surfaces for deletion instead of hiding every other pad.padLoadhandler is wrapped so it always emits a terminalresults:padLoad(the SPA can't hang) and never bubbles to the process-level rejection handler (the server can't exit from this path).Tests
tests/backend/specs/admin/padLoadResilience.ts: injects a corruptpad:*record next to a readable pad and asserts the listing still returns both. Fails without the fix (no results:padLoad reply within 10s+server Exiting), passes with it.tests/frontend-new/admin-spec/admin_pads_page.spec.ts: create/open a pad → it appears in the welcome-page recent-pads list → it appears in the/adminManage-pads UI.Local gates: 7/7 admin backend specs pass (5 existing + 2 new), both Playwright admin specs pass,
ts-checkclean.🤖 Generated with Claude Code