# Rollup example commands
Runnable examples for inventory, digest generation, summary routing, the local web UI, and local tooling.
**The rollup** is the weekly Markdown + HTML digest Rollup writes to `--output-dir`
(default `~/Documents/rollup-outputs`). Each run also copies `rollup_logo.png` and
`favicon.ico` beside the HTML file, and by default runs every output writer
(xteink / txt / json / epub). See [OUTPUT_WRITERS.md](OUTPUT_WRITERS.md).
See [README.md](../README.md) for setup, safety guarantees, and configuration defaults.
Optional sticky config: [CONFIG.md](CONFIG.md). Product shape: [CONTRACT.md](CONTRACT.md).
Position: [COMPARISON.md](COMPARISON.md). Roadmap: [ROADMAP.md](ROADMAP.md).
**Default deploy:** [DOCKER.md](DOCKER.md).
Most recipes below use the host CLI (`python -m rollup` / `rollup`). With Docker, run the same commands via `docker compose exec rollup …` (paths inside the container are under `/data/*` — see [DOCKER.md](DOCKER.md)).
Run from the project root with the virtualenv active (host Python path only):
```bash
cd /path/to/rollup
source .venv/bin/activate
```
**Default digest mode** needs no Ollama server and makes no network calls unless you pass `--linkedin` (or enable `[linkedin]` in TOML), `--reddit` (or enable `[reddit]` in TOML), webpage queue fetches (on by default; `--no-webpage` to skip), `--scholar-mode detailed` (or `[scholar].mode = "detailed"`), or `--ollama`. Pass `--ollama` only when you want LLM summaries from a local Ollama instance.
If you pass summary flags (for example `--summary-profile`) without `--ollama`, Rollup ignores them and prints a warning.
Mode shorthand: `--dry-run` parses only and writes nothing; preview-summary mode
is the normal no-Ollama digest; `--ollama` enables entry LLM summaries;
`--final-review` runs whole-digest QA and can use Ollama even without `--ollama`;
`--final-review-mode report` writes advisory QA only, while `apply` applies
validated summary-only fixes.
## Install
### Default: Docker
```bash
mkdir -p ~/.config/rollup ~/Documents/rollup-outputs ./state ./logs
touch ~/.config/rollup/config.toml ~/.config/rollup/env
chmod 600 ~/.config/rollup/env
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up -d --build
open http://localhost:8765
```
Full guide: [DOCKER.md](DOCKER.md).
### Advanced: host Python / venv
| Method | Command |
|--------|---------|
| PyPI (when published) | `pip install rollup` |
| Git checkout | `pip install .` |
| Editable + tests | `pip install -e ".[dev,web]"` |
| Web UI only | `pip install 'rollup[web]'` |
| LiteLLM providers | `pip install 'rollup[llm]'` |
| EPUB writer | `pip install 'rollup[epub]'` |
| uv | `uv sync --extra dev --extra web` |
## Inventory
Discover mbox folders and message counts (read-only; no body parsing):
```bash
python -m rollup inventory
python -m rollup inventory --root tests/fixtures/Newsletters.sbd
python -m rollup inventory --json-out ./output/inventory.json
# After setting root in ~/.config/rollup/config.toml, bare inventory uses that path:
python -m rollup inventory
```
## Digest without Ollama (default)
Preview and generate digests with no Ollama server and no network calls (unless LinkedIn, Reddit, or webpage fetch is enabled). `--no-ollama` is optional — it is the default when neither `--ollama` nor `--no-ollama` is passed.
```bash
python -m rollup digest
python -m rollup digest --root tests/fixtures/Newsletters.sbd
python -m rollup digest --lookback-days 7
python -m rollup digest --dry-run --root tests/fixtures/Newsletters.sbd
python -m rollup digest --folder tech --exclude-folder hoops
python -m rollup digest --include-seen-undated
python -m rollup digest --cron --root tests/fixtures/Newsletters.sbd
python -m rollup digest --no-grouping --root tests/fixtures/Newsletters.sbd
python -m rollup digest --grouping-report --root tests/fixtures/Newsletters.sbd
```
After a digest, browse the indexed archive (requires `pip install 'rollup[web]'`):
```bash
python -m rollup web --open
```
## LinkedIn content searches (opt-in network)
Author-list (`fromMember`) content-search URLs become digest sections. Default
layout is **`feed`** (one `linkedin:feed` section). Set `layout = "per_search"`
to keep each named search as `linkedin:` (needed for `--folder linkedin:general`
below). Fetch uses LinkedIn Voyager with **your** browser session.
Link posts also fetch the linked article body by default (external blogs, Pulse).
Reference: [CONFIG.md](CONFIG.md#linkedin-content-searches-optional).
### 1. Save a fromMember search URL
1. Log in at [linkedin.com](https://www.linkedin.com) in a desktop browser.
2. Open **Search**, switch the results type to **Content** (not People or Jobs).
3. Filter by the people you want (`fromMember`). The address bar must contain
`/search/results/content/` and `fromMember=` with `ACo…` ids.
4. Copy that URL. Save it on the web **LinkedIn** page (`/linkedin`) with a
display name, or in TOML. Store URLs only — never cookies.
```toml
# ~/.config/rollup/config.toml (or ./rollup.toml)
[linkedin]
enabled = true
article_fetch = true # default; set false to skip linked-article HTTP
layout = "per_search" # named section per search; omit for default feed (`linkedin:feed`)
[linkedin.searches.general]
url = "https://www.linkedin.com/search/results/content/?origin=FACETED_SEARCH&datePosted=%5B%22past-week%22%5D&fromMember=%5B%22ACoAAAMN5aEBk7L5BGyjHbFsDr40zYqwuSB7tlw%22%2C%22ACoAAA5GcN4BlMrjuK1OVX4Q63rShHLMZuQ1Qyg%22%5D"
display_name = "General"
enabled = true
```
Keyword-only search, company pages, follows, and mentions are not supported yet.
### 2. Copy session cookies (li_at and JSESSIONID)
Voyager needs two cookies from the **same** logged-in browser pane. They are
session secrets: put them in the process environment only. Never TOML, Settings,
git, or screenshots you share.
| Cookie | Environment variable | Role |
|--------|----------------------|------|
| `li_at` | `ROLLUP_LINKEDIN_LI_AT` | Session |
| `JSESSIONID` | `ROLLUP_LINKEDIN_JSESSIONID` | Voyager CSRF (`ajax:…`) |
**Chrome / Edge / Brave**
1. Stay logged in on `https://www.linkedin.com`.
2. Open DevTools (`Cmd+Option+I` on macOS, `F12` elsewhere).
3. **Application** (Chrome/Edge) or **Application / Storage** → **Cookies** →
`https://www.linkedin.com`.
4. Find `li_at`. Copy the **Value** column (a long `AQE…` string).
5. Find `JSESSIONID`. Copy the value. It usually looks like `ajax:123…` or
`"ajax:123…"`. Surrounding quotes are optional; Rollup accepts either.
**Firefox**
1. DevTools (`Cmd+Option+I`) → **Storage** → **Cookies** → `https://www.linkedin.com`.
2. Copy `li_at` and `JSESSIONID` values as above.
**Safari**
1. Enable **Develop** menu (Settings → Advanced → Show features for web developers).
2. **Develop → Show Web Inspector → Storage → Cookies** for `linkedin.com`.
3. Copy `li_at` and `JSESSIONID`.
They rotate together. After a 401, refresh **both** from the same pane. Treat a
new `li_at` with a stale `JSESSIONID` as expired.
### 3. Export and run
In the same shell you use for `rollup` (do not commit these lines):
```bash
export ROLLUP_LINKEDIN_LI_AT='AQE…' # li_at value
export ROLLUP_LINKEDIN_JSESSIONID='ajax:…' # JSESSIONID value
```
Dry-run first (no LinkedIn HTTP; warns if cookies are missing):
```bash
python -m rollup digest --linkedin --dry-run
```
Then fetch. `--linkedin` is required unless `[linkedin].enabled = true` in TOML:
```bash
python -m rollup digest --linkedin --folder linkedin:general
python -m rollup digest --linkedin --lookback-days 7
python -m rollup digest --linkedin --linkedin-refresh # bypass listing cache
python -m rollup digest --no-linkedin # mail only, even if TOML enables LinkedIn
python -m rollup digest --linkedin --no-linkedin-article-fetch # posts only, no article HTTP
```
A successful fetch logs `Fetching LinkedIn fromMember feed (N authors) via Voyager`.
With `layout = "per_search"` (as in the TOML above), posts land in `linkedin:`
(here `linkedin:general`) as **standalone** entries. Default `feed` layout uses
`linkedin:feed` instead. Link posts use the article title as the subject when
Voyager exposes one, and append the fetched article body after the commentary teaser.
Mute a noisy author:
```bash
python -m rollup sources disable li:member:ACoAAA5GcN4BlMrjuK1OVX4Q63rShHLMZuQ1Qyg
```
For launchd/cron, `~/.config/rollup/env` is loaded at CLI startup. You can also
put the same two variables in the job environment (plist `EnvironmentVariables`).
See [CRON.md](CRON.md#environment-variables-network-sources).
If fetch fails, see
[TROUBLESHOOTING.md](TROUBLESHOOTING.md#linkedin-fetch-failed-401--429--checkpoint).
## Reddit subreddits (opt-in network)
Public listings via a transport ladder (optional OAuth JSON → public JSON →
www RSS → old.reddit RSS). No Reddit account is required. Sections land in
`reddit:feed` or `reddit:` depending on `layout`. Listings reuse a SQLite
cache within `fetch_ttl_hours` (default 24). Reference:
[CONFIG.md](CONFIG.md#reddit-subreddits-optional).
### TOML
```toml
[reddit]
enabled = true
layout = "feed" # feed | per_source
sort = "hot"
[reddit.subs.python]
enabled = true
[reddit.subs.machinelearning]
enabled = true
mode = "posts"
limit = 5
```
Or use **Configuration Centre → Reddit** (`/reddit`) to add subs, toggle inclusion,
and set per-sub mode/sort/cap (saved in TOML). The Reddit page and Run Studio show
the estimated fetch wait (70s between subs; ~1 request/minute).
### CLI
Dry-run first (no Reddit HTTP):
```bash
python -m rollup digest --reddit --dry-run
```
Then fetch. `--reddit` is required unless `[reddit].enabled = true` in TOML:
```bash
python -m rollup digest --reddit --lookback-days 7
python -m rollup digest --reddit --folder reddit:feed
python -m rollup digest --reddit --reddit-refresh # bypass listing cache
python -m rollup digest --no-reddit # mail only, even if TOML enables Reddit
```
Mute a noisy subreddit:
```bash
rollup sources disable reddit:sub:python
```
Optional script-app OAuth (never TOML) goes in `~/.config/rollup/env` — see [CONFIG.md](CONFIG.md#optional-oauth-environment-only). Failures: [TROUBLESHOOTING.md](TROUBLESHOOTING.md#reddit-fetch-failed-429--missing-sub).
## Google Scholar alerts
Scholar emails already in Thunderbird. Default mode keeps them as item-list mail
(no extra network). Detailed mode fetches each paper and summarises it.
Reference: [CONFIG.md](CONFIG.md#google-scholar-alerts-optional-detailed-mode).
```toml
[scholar]
mode = "detailed"
max_papers_per_email = 8
max_fetches_per_run = 40
```
```bash
python -m rollup digest --scholar-mode default
python -m rollup digest --ollama --scholar-mode detailed
python -m rollup digest --scholar-mode detailed --dry-run # parse papers, no HTTP
```
Or set mode and caps in **Settings → Google Scholar**. Registry source detail
shows a banner when the source looks like Scholar.
## Webpage articles (opt-in network)
HTTPS article URLs live in SQLite (`webpage_queue`), not TOML. Add URLs
from the web **Articles** page (`/articles`), the Firefox **Add to Rollup**
add-on, or enqueue programmatically. The next
digest fetches pending rows once into folder `webpage:queue` and caches the body.
Later digests reuse that cache and include an article when it was **saved within
the lookback window** (same rule as email dates). Failed fetches stay **failed**
for retry. Pass `--no-webpage` to skip. Reference: [CONFIG.md](CONFIG.md#webpage-articles-optional).
### Web UI
```bash
python -m rollup web --open
# Articles → add HTTPS URL → Run Studio → run digest
```
### Firefox extension
Load [`extension/firefox`](../extension/firefox/README.md) as a temporary add-on
(`about:debugging#/runtime/this-firefox`). Copy the capture token from
**Articles**, paste it into add-on options, then use the toolbar or context menu
**Add to Rollup**. The add-on POSTs to `/articles/capture`; `rollup web` must be
running. Capture is ingest-only (the next digest fetches the URL).
### CLI
Digest with fixture mail plus any pending queue URLs (network fetch for pending rows):
```bash
python -m rollup digest \
--root tests/fixtures/Newsletters.sbd \
--mail-root tests/fixtures \
--lookback-days 3650 \
--no-ollama
python -m rollup digest --no-webpage # skip webpage queue even when pending rows exist
python -m rollup digest --folder webpage:queue --no-ollama # queue only
```
Mute a noisy site in the source registry:
```bash
python -m rollup sources disable web:host:example.com
```
## Doctor
```bash
python -m rollup doctor --root tests/fixtures/Newsletters.sbd
python -m rollup doctor --json --root tests/fixtures/Newsletters.sbd
python -m rollup doctor --full --root tests/fixtures/Newsletters.sbd
```
Inspect merged TOML + profile:
```bash
python -m rollup config print
python -m rollup --config ./rollup.toml config print --profile daily
```
## Cron helpers (launchd preferred on macOS)
```bash
python -m rollup cron print-launchd --python "$(which python)" --workdir .
python -m rollup cron print-crontab --python "$(which python)" --workdir .
python -m rollup cron status
```
See [docs/CRON.md](CRON.md) for weekly non-AI digest scheduling.
## Digest with Ollama (recommended full run)
Requires a running local Ollama server (`--ollama` enables network calls to loopback by default).
**Recommended full run** (all folders, 7-day lookback, per-type model routing):
```bash
python -m rollup digest --ollama --summary-routing-report
```
`--ollama` alone enables type routing by default. Use `--summary-routing-report` to print which profiles and models were used.
### LiteLLM / external providers
Requires `pip install 'rollup[llm]'` and provider credentials in the environment
(for example `OPENAI_API_KEY`). `--ollama` still enables LLM calls; choose the
transport with `--llm-provider`:
```bash
python -m rollup digest --ollama \
--llm-provider litellm \
--llm-model openai/gpt-4o \
--summary-routing-report
```
Optional OpenAI-compatible / proxy base (CLI only, never sticky):
```bash
python -m rollup digest --ollama \
--llm-provider litellm \
--llm-model openai/gpt-4o \
--llm-api-base http://127.0.0.1:4000
```
Do not use LiteLLM model strings that route native Ollama (`ollama/…`); use
`--llm-provider ollama` instead. Cron/launchd snippets may include
`--llm-provider` / `--llm-model`; secrets must be present in the scheduler
environment.
### Inspect profiles and routes
```bash
python -m rollup digest --list-summary-profiles
python -m rollup digest --list-newsletter-types
```
### Routing modes
Single profile for the whole digest:
```bash
python -m rollup digest --ollama --summary-profile standard
python -m rollup digest --ollama --summary-profile deep
```
Explicit per-type routing (same as `--ollama` default):
```bash
python -m rollup digest --ollama --summary-type-routing --summary-routing-report
```
Disable per-type routing and use the `standard` profile for every message:
```bash
python -m rollup digest --ollama --no-summary-type-routing --summary-routing-report
```
Compare multiple profiles side by side (writes one rollup set per profile):
```bash
python -m rollup digest --ollama --summary-variants rough,standard,deep --summary-routing-report
```
Variant mode writes files such as:
- `output/2026-07-02-newsletter-digest.rough.md`
- `output/2026-07-02-newsletter-digest.deep.html`
- `output/rollup_logo.png`
- `output/favicon.ico`
### Smoke tests and cache control
```bash
python -m rollup digest --ollama --folder tech --lookback-days 7 --summary-routing-report
python -m rollup digest --ollama --rebuild-summaries --folder tech --lookback-days 7
python -m rollup digest --ollama --rebuild-summaries --summary-routing-report
```
Re-run without `--rebuild-summaries` to confirm cache hits in the stats block.
### Effort presets (machine power)
Swap the whole summary ladder and companion defaults in one flag:
```bash
python -m rollup digest --list-efforts
python -m rollup digest --ollama --effort light --lookback-days 7
python -m rollup digest --ollama --effort high --lookback-days 7 --summary-routing-report
python -m rollup doctor --ollama --effort high --network
```
`balanced` matches today's built-in defaults. Override models inside an effort with `[efforts.high]` (and friends) in TOML or Settings. Explicit `--ollama-model` / `--final-review-model` / `--max-chars-for-llm` still win over the preset. Do not combine `--effort` with `--summary-profile-set`.
Use one model for every profile on a single run (not sticky):
```bash
python -m rollup digest --ollama --effort balanced --single-model qwen2.5:7b --lookback-days 7
python -m rollup doctor --ollama --single-model qwen2.5:7b --network
```
### XTEINK e-ink output
Write a second, device-optimized digest (short lines, no URLs) beside the normal files via the **xteink** output writer:
```bash
python -m rollup digest --xteink --lookback-days 7
python -m rollup digest --output xteink --lookback-days 7
python -m rollup digest --xteink --ollama --effort high --lookback-days 7
```
Outputs look like `…-newsletter-digest.xteink.md`. Skipped under `--dry-run`. See [XTEINK_USAGE.md](XTEINK_USAGE.md).
### JSON, TXT, and EPUB outputs
By default these run automatically with every digest. To write only a subset
(or Markdown/HTML alone):
```bash
python -m rollup digest --output json --output txt --lookback-days 7
python -m rollup digest --output none --lookback-days 7
# EPUB needs: pip install 'rollup[epub]'
python -m rollup digest --output epub --lookback-days 7
```
Same run stem as the core digest with `.json` / `.txt` / `.epub` extensions. See [OUTPUT_WRITERS.md](OUTPUT_WRITERS.md).
### Custom profile sets
Export built-in profiles, edit model names to match your local Ollama library, then run:
```bash
python -m rollup digest --export-summary-profile-set ./output/summary_profiles.json
python -m rollup digest --ollama --summary-profile-set ./output/summary_profiles.json --summary-routing-report
```
Each profile supports Ollama generation fields:
| Field | Default | Notes |
|-------|---------|-------|
| `num_predict` | `2048` | Max generated tokens (`options.num_predict` in the Ollama request) |
| `think` | `false` | Top-level Ollama `think` flag: `false`/`true` for Qwen3; GPT-OSS needs `"low"`/`"medium"`/`"high"` (booleans are ignored) |
| `options` | `{}` | Additional Ollama model options (temperature is set separately via `temperature`) |
Example fragment after export — adjust model names and generation settings:
```json
{
"schema_version": 1,
"default_profile": "standard",
"profiles": {
"max": {
"provider": "ollama",
"model": "qwen3.6:27b",
"prompt_style": "deep",
"temperature": 0.2,
"num_ctx": 65536,
"timeout_seconds": 600,
"num_predict": 2048,
"think": false,
"enabled": true,
"description": "Long essays",
"options": {}
}
},
"type_routes": {
"essay": "max"
}
}
```
Do **not** put `think` or `num_predict` inside `options` — Ollama ignores `think` there on `/api/generate`, which causes empty summaries on thinking models. For GPT-OSS, set `"think": "low"` (not `false`) and keep enough `num_predict` headroom for reasoning + the visible summary.
Inspect loaded profiles:
```bash
python -m rollup digest --list-summary-profiles
```
Example model pulls for the built-in profile set:
```bash
ollama pull llama3.2:3b
ollama pull qwen2.5:7b
ollama pull gpt-oss:20b
ollama pull qwen3.6:27b
```
## Live-run workflow
Incremental checks before a full live digest:
```bash
python -m rollup inventory --root tests/fixtures/Newsletters.sbd
python -m rollup digest --root tests/fixtures/Newsletters.sbd
# Prefer sticky paths in ~/.config/rollup/config.toml, then:
python -m rollup inventory
python -m rollup digest --folder tech
python -m rollup digest --profile weekly
python -m rollup digest
python -m rollup digest --ollama --folder tech --summary-routing-report
python -m rollup digest --ollama --summary-routing-report
```
### Migrate from hardcoded paths
```toml
# ~/.config/rollup/config.toml
root = "~/email/gmail/Newsletters.sbd"
mail_root = "~/email/gmail"
[folders.tech]
emoji = "💻"
accent = "#4a7fd4"
```
```bash
rollup config print
rollup doctor
rollup digest
```
Explicit `--no-ollama` is equivalent to omitting both `--ollama` and `--no-ollama`.
Optional gitignored local mail copy:
```bash
cp -R "$HOME/email/gmail/Newsletters.sbd" ./fixtures/Newsletters.sbd
python -m rollup digest --root ./fixtures/Newsletters.sbd
```
## Ollama validation sequence
```bash
python -m rollup digest --list-summary-profiles
python -m rollup digest --list-newsletter-types
python -m rollup digest --ollama --folder tech --lookback-days 7 --summary-routing-report
python -m rollup digest --ollama --summary-profile standard --folder tech --lookback-days 7
python -m rollup digest --ollama --rebuild-summaries --folder tech --lookback-days 7
python -m rollup digest --ollama --summary-routing-report
```
Stop Ollama and re-run a smoke command to confirm preview fallback without crashing:
```bash
python -m rollup digest --ollama --folder tech --lookback-days 7
```
## Final review (editorial QA)
Run a whole-digest editorial QA pass after assembly. Report-only by default: writes a JSON sidecar and does **not** change digest content. Advisory only.
```bash
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --final-review-profile concise
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --final-review-report ./output/review.json
python -m rollup digest --root ./fixtures/Newsletters.sbd --final-review --no-final-review-cache
```
Final review does not require `--ollama` on the digest summarisation path (it uses Ollama independently when enabled). Apply mode (`--final-review-mode apply`) applies validated summary-only patches linked to `safe_auto_fix: true` issues; missing fingerprint echoes skip the whole set. Under `--cron`, apply also requires `--final-review-allow-cron-apply` and conservative whole-set caps. When enabled, a short QA summary also appears in the digest’s collapsed “Digest generation details” section at the end.
```bash
# Apply mode (interactive / manual)
# mail_root must contain the newsletter root (omit --mail-root to infer the .sbd parent)
rollup digest --root tests/fixtures/Newsletters.sbd --final-review \
--final-review-mode apply \
--output-dir /tmp/rollup-out --state-dir /tmp/rollup-state
# Group-level LLM summaries (opt-in; requires Ollama)
rollup digest --root tests/fixtures/Newsletters.sbd --ollama --group-summaries \
--output-dir /tmp/rollup-out --state-dir /tmp/rollup-state
```
## Source registry
Persistent per-newsletter controls (see [SOURCES.md](SOURCES.md)):
```bash
# After a digest has observed senders
rollup sources list --state-dir /tmp/rollup-state
rollup sources show from:alerts@github.com --state-dir /tmp/rollup-state --json
# Disable a noisy source
rollup sources disable from:noisy@example.com --state-dir /tmp/rollup-state
# Force a daily newsletter into weekly sender batches
rollup sources set list:news.example.com --grouping sender_batch --priority 80 \
--state-dir /tmp/rollup-state
# Always surface undated notifications from a source (still within lookback)
rollup sources set from:alerts@github.com --always-surface --state-dir /tmp/rollup-state
# Override a misclassified type
rollup sources set from:editor@daily.example --type essay --state-dir /tmp/rollup-state
# Backup / restore overrides
rollup sources export --out /tmp/sources.json --state-dir /tmp/rollup-state
rollup sources import --from /tmp/sources.json --state-dir /tmp/rollup-state
```
## Local web UI
**Preferred:** Docker Compose — see [Docker](#docker) below and [DOCKER.md](DOCKER.md).
### Advanced: host Python
Browse indexed rollups, rate emails, and review newsletter quality. Binds to **loopback only** (`127.0.0.1` by default). Pass `--allow-non-loopback-bind` only for Docker port mapping. Requires the optional Flask extra.
Bring-up (install → digest that indexes into state → start the UI):
```bash
pip install 'rollup[web]' # or from a checkout: pip install -e '.[web]'
python -m rollup digest --root tests/fixtures/Newsletters.sbd
python -m rollup web --open
```
**Configuration Centre** (`/settings`) edits the real digest TOML (paths, profiles, writers, folder themes). **Run Studio** (`/run`) previews the effective run, dry-runs discovery, and starts a digest without composing CLI by hand. **Articles** / **LinkedIn** / **Reddit** manage those sources. See [WEB.md](WEB.md) and [CONFIG.md](CONFIG.md).
Variants:
```bash
python -m rollup web
python -m rollup web --host 127.0.0.1 --port 8765 --open
python -m rollup web --host 0.0.0.0 --port 8765 --allow-non-loopback-bind # Docker only
python -m rollup web --config ~/.config/rollup/config.toml --open
python -m rollup web reindex --state-dir ./state --output-dir ./output
```
See [WEB.md](WEB.md) for security model, Settings / Run Studio / Articles / LinkedIn / Reddit, quality score, and backup notes.
Reader-body cache (same SQLite; Admin can prune/backfill too):
```bash
python -m rollup bodies stats
python -m rollup bodies check
python -m rollup bodies backfill --dry-run
```
## Docker
Default deploy: web + digest in one container while sharing the same config, state, output, and mail paths as a host CLI. See [DOCKER.md](DOCKER.md).
```bash
mkdir -p ~/.config/rollup ~/Documents/rollup-outputs ./state ./logs
touch ~/.config/rollup/config.toml ~/.config/rollup/env
chmod 600 ~/.config/rollup/env
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up -d --build
open http://localhost:8765
```
The image binds with `--allow-non-loopback-bind` so Flask can listen on `0.0.0.0` inside the container. Compose publishes `127.0.0.1:8765:8765`; access from the host uses `http://localhost:8765` (Host-header loopback checks unchanged). Override the port mapping only if you intentionally need LAN access.
Do not run native `rollup web` and Docker web at the same time.
Fixture smoke test without real mail:
```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
curl -fsS http://127.0.0.1:8765/rollups
```
## Benchmark local models
Compare local Ollama-compatible models on fixed prompts:
```bash
python scripts/benchmark_ollama_models.py \
--models llama3.2:3b,qwen2.5:7b,gpt-oss:20b,qwen3.6:27b \
--runs 2 \
--num-ctx 16384 \
--out benchmarks/ollama_benchmark.json \
--markdown-out benchmarks/ollama_benchmark.md
```
## Tests and fixtures
```bash
python -m pytest tests/ -v
python tests/generate_fixtures.py
```