docs: document shared search pattern for feed, gists, and project listings
Add a reusable `text_search_clause` helper in `database.py` that builds a SQLAlchemy `or_` of `ilike` clauses over specified columns, returning `None` when search is blank or the table lacks the columns. Wire it into `get_feed_posts`, `get_gists_list`, and the projects listing so all three public index pages support free-text search via a `search` query parameter. Introduce `_sidebar_search.html` as the single search-box partial, included at the top of each listing's left filter panel with `_action`, `_placeholder`, and `_hidden` locals to preserve active category/tab filters on submit. Expose the `search` field on `FeedOut`, `GistsOut`, and `ProjectsOut` API schemas with corresponding OpenAPI documentation. Update `CLAUDE.md` to note the rate-limiter exemption for `GET`/`HEAD` and the `/openai` gateway, and refresh `README.md` route tables to mention the new search capability on `/feed`, `/gists`, and `/projects`.
This commit is contained in:
@@ -59,7 +59,7 @@ Tests run on port 10501 with a tempfile SQLite DB and a dedicated `DEVPLACE_DATA
|
||||
## Architecture
|
||||
|
||||
### Request pipeline
|
||||
`main.py` mounts `/static`, registers every router with its prefix, installs middlewares (`X-Robots-Tag`/`X-Content-Type-Options` security headers, a per-IP rate limit held in an in-process `defaultdict`, and a maintenance gate), and registers global 404/500 handlers that render `error.html` via the shared templates instance. The rate limit reads `rate_limit_per_minute` / `rate_limit_window_seconds` from `site_settings` (default 60/60s, floored to `max(1, …)`). The maintenance middleware short-circuits non-admin requests with a 503 `error.html` when `maintenance_mode="1"`, but always allows `/static`, `/avatar`, `/auth`, `/admin` and admin users so an operator can never lock themselves out.
|
||||
`main.py` mounts `/static`, registers every router with its prefix, installs middlewares (`X-Robots-Tag`/`X-Content-Type-Options` security headers, a per-IP rate limit held in an in-process `defaultdict`, and a maintenance gate), and registers global 404/500 handlers that render `error.html` via the shared templates instance. The rate limit reads `rate_limit_per_minute` / `rate_limit_window_seconds` from `site_settings` (default 60/60s, floored to `max(1, …)`). **It applies only to mutating methods (`POST`/`PUT`/`DELETE`/`PATCH`); reads (`GET`/`HEAD`) and the whole `/openai` gateway are exempt** - so a burst of `GET` requests is never throttled (verified against production: 400 concurrent `GET /` all `200`, while 150 concurrent `POST /auth/login` allowed exactly 60 then returned `429`). The bucket is keyed on the `X-Real-IP` header (set by the reverse proxy) falling back to `request.client.host`, and an over-limit request gets a `429` carrying `Retry-After: <window>` (HTML body, or the shared JSON error shape for `Accept: application/json` clients). The maintenance middleware short-circuits non-admin requests with a 503 `error.html` when `maintenance_mode="1"`, but always allows `/static`, `/avatar`, `/auth`, `/admin` and admin users so an operator can never lock themselves out.
|
||||
|
||||
`@app.on_event("startup")` calls `init_db()` and, unless `DEVPLACE_DISABLE_SERVICES` is set, registers `NewsService` with `service_manager` and kicks off `start_all()` as an asyncio task. `@app.on_event("shutdown")` calls `service_manager.stop_all()`.
|
||||
|
||||
@@ -107,6 +107,7 @@ Docs (`routers/docs.py` `DOCS_PAGES`) entries take optional `admin: True` (hidde
|
||||
- **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".
|
||||
|
||||
### Content rendering pipeline
|
||||
`ContentRenderer.js` runs on every element with `data-render`, in this exact order: emoji shortcode → markdown via `marked` → **`DOMPurify.sanitize`** → `highlight.js` on `<pre><code>` → image URL → YouTube URL → autolink. Uses a `NodeIterator` that skips `CODE`/`PRE`/`SCRIPT`/`STYLE` so URLs inside code blocks are never rewritten.
|
||||
|
||||
Reference in New Issue
Block a user