206 lines
7.9 KiB
Markdown
206 lines
7.9 KiB
Markdown
# scholarr
|
|
|
|
Container-first, self-hosted scholar tracking in the spirit of the `*arr` ecosystem, with strict multi-user isolation and an intentionally small operational footprint.
|
|
|
|
## Current Scope
|
|
|
|
- Admin-managed users only (no public signup)
|
|
- Session auth with CSRF protection and login rate limiting
|
|
- Per-user scholar tracking list (add, enable/disable, delete)
|
|
- Per-user automation settings
|
|
- Manual per-user ingestion runs from dashboard
|
|
- Per-user run history and "new since last run" publication stream
|
|
- Publications library view with `new` and `all` modes plus scholar filtering
|
|
- Dedicated run diagnostics pages (`/runs`, `/runs/{id}`) with failed-only filtering
|
|
- Continuation queue visibility + controls on runs page (`retry`, `drop`, `clear`)
|
|
- Baseline-aware discovery tracking (`new` = discovered in latest completed run)
|
|
- Read state is user-controlled (`Mark All Read`), including baseline items
|
|
- Multi-page profile ingestion (`cstart`) to capture full publication lists
|
|
- Dashboard rendered via a presentation-layer view model and section template registry
|
|
- Structured application logging with request IDs and field redaction
|
|
- Ingestion overlap guard via Postgres advisory locks
|
|
- Network-error retry support for ingestion attempts
|
|
- Automatic continuation queue for resumable/limit-hit scholar runs
|
|
- Background scheduler for per-user automatic runs
|
|
- User self-service password changes
|
|
- Admin user management (create users, activate/deactivate, reset passwords)
|
|
- PostgreSQL + Alembic migrations + containerized test workflow
|
|
- Versioned backend API (`/api/v1`) for frontend decoupling (same-origin cookie session)
|
|
|
|
## Tech Stack
|
|
|
|
- Python 3.12+
|
|
- FastAPI
|
|
- PostgreSQL 15+
|
|
- SQLAlchemy 2 + Alembic
|
|
- Jinja2 templates + modular theme tokens
|
|
- `uv` for dependency and runtime workflow
|
|
- Docker / Docker Compose for development and CI parity
|
|
|
|
## Quick Start
|
|
|
|
1. Copy environment defaults:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
2. Bootstrap the first admin on startup (recommended for first run):
|
|
|
|
```bash
|
|
export BOOTSTRAP_ADMIN_ON_START=1
|
|
export BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
|
export BOOTSTRAP_ADMIN_PASSWORD=change-me-now
|
|
```
|
|
|
|
3. Start the stack:
|
|
|
|
```bash
|
|
docker compose up --build
|
|
```
|
|
|
|
4. Open:
|
|
|
|
- Login: `http://localhost:8000/login`
|
|
- Dashboard: `http://localhost:8000/`
|
|
- Healthcheck: `http://localhost:8000/healthz`
|
|
|
|
5. After first successful admin login, disable auto-bootstrap:
|
|
|
|
```bash
|
|
export BOOTSTRAP_ADMIN_ON_START=0
|
|
```
|
|
|
|
## Admin Bootstrap (Manual)
|
|
|
|
You can create/update an admin account without restarting the app:
|
|
|
|
```bash
|
|
docker compose run --rm app uv run python scripts/bootstrap_admin.py \
|
|
--email admin@example.com \
|
|
--password change-me-now \
|
|
--force-password
|
|
```
|
|
|
|
Notes:
|
|
- If the user does not exist, this creates an active admin account.
|
|
- If the user exists, it ensures admin + active flags.
|
|
- `--force-password` resets the password for existing users.
|
|
|
|
## Test Workflow
|
|
|
|
- Integration/smoke tests run against `TEST_DATABASE_URL`.
|
|
- If `TEST_DATABASE_URL` is empty, test runs derive an isolated database from `DATABASE_URL` by appending `_test`.
|
|
- This keeps app data in `DATABASE_URL` untouched during test runs.
|
|
- Parser regression tests are pinned to captured real-world fixture pages in `tests/fixtures/scholar/regression`.
|
|
|
|
- Unit tests:
|
|
|
|
```bash
|
|
docker compose run --rm app uv run pytest tests/unit
|
|
```
|
|
|
|
- Integration tests:
|
|
|
|
```bash
|
|
docker compose run --rm app uv run pytest -m integration
|
|
```
|
|
|
|
- Smoke checks (container + DB + migration sanity):
|
|
|
|
```bash
|
|
./scripts/smoke_compose.sh
|
|
```
|
|
|
|
CI runs migration, unit, and integration checks on push/PR via `.github/workflows/ci.yml`.
|
|
|
|
## Scholar Ingestion Notes
|
|
|
|
- Current ingestion targets public profile pages: `/citations?user=<id>`.
|
|
- Runs are intentionally conservative and prioritize reliability over aggressive scraping.
|
|
- Ingestion follows pagination (`cstart`) to collect all reachable profile pages.
|
|
- If pagination cannot be completed (max pages hit, cursor stall, or page-state failure), the scholar result is marked partial with explicit debug context.
|
|
- Resumable partial/failure states are automatically queued and retried by the scheduler with bounded backoff.
|
|
- Queue items keep explicit status (`queued`, `retrying`, `dropped`) and last-error context for UI diagnostics.
|
|
- Failed scholar attempts persist structured debug context in `crawl_runs.error_log` (state reason, fetch metadata, marker evidence, and compact response excerpt).
|
|
- Only resumable states are retried automatically (`network_error` and continuation-eligible pagination truncations); blocked/layout states are recorded immediately as failures.
|
|
|
|
## API v1 (Frontend Prep)
|
|
|
|
- Base path: `/api/v1`
|
|
- Auth model: same-origin cookie session (no token auth added)
|
|
- CSRF model: required for unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`) via `X-CSRF-Token` header.
|
|
- Bootstrap CSRF via `GET /api/v1/auth/csrf` (works for anonymous and authenticated sessions).
|
|
- You can also fetch `csrf_token` from `GET /api/v1/auth/me` after login.
|
|
- Manual run idempotency is supported via `Idempotency-Key` header on `POST /api/v1/runs/manual`.
|
|
- Response envelopes:
|
|
- Success: `{"data": ..., "meta": {"request_id": "..."}}`
|
|
- Error: `{"error": {"code": "...", "message": "...", "details": ...}, "meta": {"request_id": "..."}}`
|
|
- Initial API domains exposed:
|
|
- `auth`: `/auth/csrf`, `/auth/login`, `/auth/me`, `/auth/change-password`, `/auth/logout`
|
|
- `admin users`: `/admin/users` (list/create), `/admin/users/{id}/active`, `/admin/users/{id}/reset-password`
|
|
- `scholars`: list/create/toggle/delete
|
|
- `settings`: get/update
|
|
- `runs`: list/detail/manual trigger + queue list/retry/drop/clear
|
|
- `publications`: list + mark-all-read
|
|
- Queue action semantics:
|
|
- `retry` is valid only for dropped items; retrying/queued states return `409`.
|
|
- `drop` returns the updated queue item (status `dropped`); dropping an already dropped item returns `409`.
|
|
- `clear` only works for dropped items and returns `{queue_item_id, previous_status, status="cleared"}`.
|
|
|
|
## Logging
|
|
|
|
- Default output is concise console logs to stdout.
|
|
- Timestamp format is compact UTC: `YYYY-MM-DD HH:MM:SSZ`.
|
|
- Every request gets an `X-Request-ID` (propagated if caller provides one).
|
|
- Sensitive fields are redacted before log emission (`password`, `csrf_token`, `cookie`, etc.).
|
|
- Configure via env:
|
|
- `LOG_LEVEL` (default `INFO`)
|
|
- `LOG_FORMAT` (`console` or `json`, default `console`)
|
|
- `LOG_REQUESTS` (`1`/`0`, default `1`)
|
|
- `LOG_UVICORN_ACCESS` (`1`/`0`, default `0`)
|
|
- `LOG_REQUEST_SKIP_PATHS` (comma-separated prefixes, default `/healthz,/static/`)
|
|
- `LOG_REDACT_FIELDS` (comma-separated additional keys)
|
|
- `SCHEDULER_ENABLED` (`1`/`0`, default `1`)
|
|
- `SCHEDULER_TICK_SECONDS` (default `60`)
|
|
- `INGESTION_NETWORK_ERROR_RETRIES` (default `1`)
|
|
- `INGESTION_RETRY_BACKOFF_SECONDS` (default `1.0`)
|
|
- `INGESTION_MAX_PAGES_PER_SCHOLAR` (default `30`)
|
|
- `INGESTION_PAGE_SIZE` (default `100`)
|
|
- `INGESTION_CONTINUATION_QUEUE_ENABLED` (`1`/`0`, default `1`)
|
|
- `INGESTION_CONTINUATION_BASE_DELAY_SECONDS` (default `120`)
|
|
- `INGESTION_CONTINUATION_MAX_DELAY_SECONDS` (default `3600`)
|
|
- `INGESTION_CONTINUATION_MAX_ATTEMPTS` (default `6`)
|
|
- `SCHEDULER_QUEUE_BATCH_SIZE` (default `10`)
|
|
|
|
## Optional Local `uv` Workflow
|
|
|
|
```bash
|
|
uv sync --extra dev
|
|
uv run pytest
|
|
uv run uvicorn app.main:app --reload
|
|
```
|
|
|
|
Update the lockfile after dependency changes:
|
|
|
|
```bash
|
|
uv lock
|
|
```
|
|
|
|
## Theming
|
|
|
|
- Theme registry: `app/theme.py`
|
|
- Semantic tokens: `app/static/theme.css`
|
|
- App styling: `app/static/app.css`
|
|
- Theme persistence: `app/static/theme.js`
|
|
- Dashboard presentation contract: `app/presentation/dashboard.py`
|
|
|
|
## Project Layout
|
|
|
|
```text
|
|
app/ FastAPI app, auth/security modules, templates, services, DB wiring, presentation view-models
|
|
app/web/ Router modules, shared web helpers, and request middleware
|
|
alembic/ Migration environment and versions
|
|
scripts/ Entrypoint, DB wait, bootstrap, smoke automation
|
|
tests/ Unit, integration, and smoke suites
|
|
```
|