Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test Coverage

on:
pull_request:

jobs:
test-coverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install . coverage

- name: Run tests with coverage
run: |
coverage run -m unittest discover -s tests
coverage report -m --fail-under=95
coverage xml
coverage html

- name: Upload coverage artifacts
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
coverage.xml
htmlcov/
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
/venv/
.venv/
venv/
__pycache__/
**/__pycache__/
*.pyc
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Repository Guidelines

## Project Structure & Module Organization
- `backend/main.py`: FastAPI service that proxies Brave Search and builds infobox data.
- `backend/cli.py`: Uvicorn runner for local/dev server (HOST/NILCH_PORT, `--debug` reload).
- `frontend/`: Static HTML/CSS/JS pages (`index.html`, `results.html`, `images.html`, etc.).
- `frontend/config.js`: Frontend API base URL for `fetch()` calls.
- `frontend/bangs.js`: DuckDuckGo bang handling used by the UI.
- `LICENSE` and `README.md` live at repo root.

## Build, Test, and Development Commands
- `python backend/cli.py --debug`: Runs the FastAPI API on `http://0.0.0.0:5001` with auto-reload.
- `python -m venv .venv && source .venv/bin/activate && pip install fastapi[standard] requests uvicorn`: Minimal setup based on imports in `backend/`.
- `open frontend/index.html` (or any static server): Loads the UI locally; update `frontend/config.js` if you run the backend elsewhere.

## Coding Style & Naming Conventions
- Python: 4-space indentation, `snake_case` functions/variables; keep routes small and readable.
- Frontend: 4-space indentation in HTML/CSS/JS; files are lowercase (e.g., `results.html`).
- Prefer simple, explicit logic over abstractions; this repo is intentionally lightweight.
- Cache behavior: `SearchCache` uses FIFO eviction when `len(cache) >= capacity`.

## Testing Guidelines
- Automated tests: `python -m unittest discover -s tests`.
- Manual smoke checks:
- Start the backend and confirm `/api/search?q=test` returns JSON.
- Open `frontend/index.html` and verify basic search flows.

## Commit & Pull Request Guidelines
- Commit history shows short, informal messages (e.g., "fix mobile pagination layout issue").
- Keep messages concise and descriptive; no strict convention enforced.
- PRs should include:
- A clear summary of behavior changes.
- Screenshots for UI changes (especially search results pages).
- Notes about any config changes (e.g., API key handling).

## Security & Configuration Tips
- `BRAVE_SEARCH_API_KEYS` in `backend/main.py` must be set locally.
- Never commit real API keys; use environment or local-only edits.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ A not-for-profit-style search engine with no ads, no AI, and all DuckDuckGo bang

nilch uses the Brave search API internally for results.

## Local development

1. Create and activate a venv with uv.
- `uv venv`
- `source .venv/bin/activate`
2. Install dependencies.
- `uv sync`
- (or `pip install fastapi[standard] requests uvicorn`)
3. Set `BRAVE_SEARCH_API_KEYS` (comma-separated if you have multiple keys).
4. Start the API server: `python -m backend.cli --port <port> --debug`.
5. Open `frontend/index.html` in a browser. If needed, update `frontend/config.js` to match the API host/port.

## Tests

- `python -m unittest discover -s tests`

## Code structure

You can find all frontend sources in `frontend/` and backend sources in `backend/`. The backend is a single Flask server containing the API, and the frontend is raw HTML/CSS/JS which calls the backend API with JS `fetch()`. Because of this separation, both can be run on completely different servers, or you could even run your own frontend locally and use the shared backend. It's also cheaper to run with my setup :)
You can find all frontend sources in `frontend/` and backend sources in `backend/`. The backend is a single FastAPI server containing the API, and the frontend is raw HTML/CSS/JS which calls the backend API with JS `fetch()`. Because of this separation, both can be run on completely different servers, or you could even run your own frontend locally and use the shared backend. It's also cheaper to run with my setup :)

Note: the in-memory search cache evicts entries FIFO once it reaches capacity.

You may need to tinker a little bit to set up your own instance, however I intend to very soon improve it to be easier.

Expand Down
Empty file added backend/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions backend/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

import argparse
import os
import uvicorn

# from backend.main import create_app


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="""
Run the FastAPI service. Optional env vars: HOST, NILCH_PORT.
""")
p.add_argument("--host", default=os.getenv("HOST", "0.0.0.0"))
p.add_argument("--port", type=int, default=int(os.getenv("NILCH_PORT", "5001")))
p.add_argument(
"--debug",
action="store_true",
help="Enable debug/dev mode (auto-reload, debug logging).",
)
return p.parse_args()


def main() -> None:
args = parse_args()

# In debug/dev, enable reload + more verbose logging.
# app = create_app(debug=False)
if args.debug:
os.environ["NILCH_DEBUG"] = "1"

uvicorn.run(
"backend.main:app",
host=args.host,
port=args.port,
reload=args.debug,
# factory=args.debug,
log_level="debug" if args.debug else "info",
# Allow forwarded headers when behind proxies/load balancers
proxy_headers=True,
forwarded_allow_ips="*",
)


if __name__ == "__main__":
main()
Loading