Calm Filters MVP: device-local personalization across feed/brief/counts
- API endpoints (feed, brief, category-counts) accept a 'prefs' JSON query param, parsed tolerantly into FilterPrefs (bad blobs never break the feed). - Feed over-fetches then applies word-boundary filters in Python and slices to the page; brief is filtered down (no refill); counts are computed over the same filtered set so browse numbers match the feed exactly. - Pause.active() coerces naive datetimes to UTC; FilterPrefs.from_dict skips malformed pauses and non-string list entries. - Static site adds the humane ladder (Not today / Less like this / Always hide) plus a Calm filters panel managing pauses, mutes, and avoid-terms in localStorage. Nothing leaves the device. - Tests now 38 (added forgiving-parse and naive-now cases). README documents it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,34 @@ Endpoints:
|
||||
The ingestion CLI stays pure-stdlib; only the `web` extra pulls in FastAPI/uvicorn,
|
||||
so the two halves can be deployed and upgraded independently.
|
||||
|
||||
## Calm Filters
|
||||
|
||||
Personal, device-local controls so a reader can stay informed without subjects
|
||||
they'd rather not see right now. Preferences live in the browser (localStorage),
|
||||
are sent to the read endpoints as a `prefs` JSON query param, and are applied
|
||||
identically to the feed, the brief, and the category counts so the numbers always
|
||||
match what's shown. The canonical shape (`goodnews/filters.py`):
|
||||
|
||||
```json
|
||||
{
|
||||
"include_topics": [], "include_flavors": [],
|
||||
"mute_topics": [], "mute_flavors": [],
|
||||
"avoid_terms": ["election", "stock market"],
|
||||
"pauses": [{"kind": "topic", "value": "health", "until": "2026-06-02T00:00:00Z"}]
|
||||
}
|
||||
```
|
||||
|
||||
The site surfaces a humane ladder rather than a settings panel of dread:
|
||||
|
||||
- **Not today** → pause that article's topic for 24h.
|
||||
- **Less like this** → ease off that flavor for ~3 days.
|
||||
- **Always hide …** → a standing mute (undoable in the Calm filters panel).
|
||||
|
||||
Avoid-terms match whole words/phrases (case- and punctuation-insensitive, no
|
||||
substring surprises like "pan" matching "pandemic"). The brief is filtered *down*
|
||||
for MVP (no refill from outside the stored brief). No accounts; the same `prefs`
|
||||
object is the clean migration path to server-side, multi-user preferences later.
|
||||
|
||||
## Deployment
|
||||
|
||||
The database is never baked into the image — the API and the ingestion CLI share
|
||||
|
||||
Reference in New Issue
Block a user