feat: add author-username search to feed/gists/projects listings and partial-index migration
DevPlace CI / test (push) Failing after 2m5s

Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
This commit is contained in:
2026-06-19 22:24:51 +00:00
parent d10f1af118
commit e393722600
22 changed files with 269 additions and 52 deletions
+1 -1
View File
@@ -136,7 +136,7 @@ Docs (`routers/docs/pages.py` `DOCS_PAGES`, re-exported from the `routers/docs`
- **Shared frontend utilities (do not re-implement these):** `Http` (`static/js/Http.js`) is the single fetch helper (`getJson`, `sendForm`, and `send` which throws `error.message` on non-2xx or a `200 {ok:false}` body); every live-update loop uses `Poller` (`new Poller(fn, intervalMs, {pauseHidden})`); async-job status polls use `JobPoller.run(statusUrl, {onDone, onFailed, onTimeout})` (`ProjectForker`, `ZipDownloader`); click-to-POST engagement controllers (`VoteManager`/`ReactionBar`/`BookmarkManager`/`PollManager`) extend `OptimisticAction` and call `this.submit(url, params, errorTarget, render)`. Floating windows (container terminals and Devii) extend `FloatingWindow`. See `AGENTS.md` -> "Shared frontend utilities (DRY)".
- CDN scripts in `base.html` (marked, highlight.js, emoji-picker-element) MUST use `defer` or `type="module"` - otherwise `wait_until="domcontentloaded"` in Playwright tests will time out.
- For clickable avatars/usernames, reuse `templates/_avatar_link.html` and `templates/_user_link.html` via `{% include %}` with `_user`, `_size`, `_size_class` locals.
- The three public listings (`/feed`, `/gists`, `/projects`) share one search box: the `templates/_sidebar_search.html` partial (included at the top of `.sidebar-card` with `_action`/`_placeholder`/`_hidden` locals) plus the `database.text_search_clause(table, search, fields)` data helper. Any new listing with a left filter panel reuses both - never re-inline an `ilike` search clause or hand-roll another search form. See `AGENTS.md` -> "Listing search".
- The three public listings (`/feed`, `/gists`, `/projects`) share one search box: the `templates/_sidebar_search.html` partial (included at the top of `.sidebar-card` with `_action`/`_placeholder`/`_hidden` locals) plus the `database.text_search_clause(table, search, fields, author_field)` data helper. Each listing matches its text fields PLUS the author username: pass `author_field="user_uid"` and the helper resolves matching usernames to uids via `database.get_uids_by_username_match(search)` and OR-includes `user_uid IN (...)` (no JOIN, no separate `ilike`). Any new listing with a left filter panel reuses both - never re-inline an `ilike` search clause, never add a JOIN for author matching, and never hand-roll another search form. See `AGENTS.md` -> "Listing search".
### Content rendering pipeline
**Server-rendered content and titles are rendered on the BACKEND for SEO** (`devplacepy/rendering.py`, exposed as the Jinja globals `render_content(text)` and `render_title(text)`, registered in `templating.py`). This is the default for all content that exists at request time: post bodies/cards, comments, news articles, project/gist descriptions, DM history, and EVERY content title. `render_content` mirrors the client `ContentRenderer.js` pipeline exactly using **mistune** (the markdown engine already used for docs prose): normalize em-dash -> hyphen, emoji shortcodes (the FULL GitHub/Discord `:name:` set, see **Emoji shortcodes** below), GFM markdown (`escape=True` so raw user HTML is escaped - the server XSS control, replacing client `DOMPurify`; `strikethrough`+`table` plugins, `hard_wrap`), then a stdlib `HTMLParser` media pass that walks text nodes (skipping `a`/`code`/`pre`) and turns bare URLs into YouTube iframes / `<img data-lightbox>` / `<video>` / `<audio>` / autolinks and `@mentions` into profile links - byte-for-byte the same markup the web component emits. Link/image URL schemes are validated (`javascript:`/`data:` -> `#`). `render_title` is the inline variant: emoji + inline markdown only, filtered to an inline-tag allowlist (no `a`/`img`/block tags), matching `dp-title`. Both are `@lru_cache`d (deterministic from input) so repeated/identical content across a feed page is rendered once - render speed. Server-rendered code blocks (`<pre><code class="language-x">`) are syntax-highlighted client-side and get the upper-right **Copy** button by `ContentEnhancer` (`highlightAll()` + `enhanceCodeBlocks()`, which calls the shared `CodeBlock.enhance(pre, {lineNumbers:false})` on every `.rendered-content pre` on load - the same primitive the docs/`dp-code` use; the generic `pre.code-has-copy > .code-copy-btn` styling lives in the global `markdown.css`). Content `<img data-lightbox>` is wired by the delegated `app.lightbox` listener - no per-element JS.