The listener opened one asyncpg connection, called add_listener, and set running = True. Nothing watched that connection afterwards. When it dropped, the subscription was gone for good while running still reported True, so the service stayed healthy in every way anything could observe and silently stopped indexing page edits. Recovery needed a manual container restart. That happened on 2026-08-08 when postgres-shared was redeployed. The sibling settings_client survived the same event because it uses asyncpg.create_pool, which replaces dead connections; a bare LISTEN connection has no such recovery. A supervisor task now waits on asyncpg's termination callback and reconnects with bounded exponential backoff, 1s doubling to a 60s cap. It retries forever rather than giving up after N attempts: a database under maintenance does come back, and a listener that stopped trying would reproduce exactly the silent deafness this exists to prevent. The termination listener is re-registered on every new connection because asyncpg clears its listener list as soon as it fires them, so a one-time registration survives exactly one drop. running is now derived from the connection rather than assigned, and stop() sets a flag the termination callback and supervisor both check so a deliberate shutdown cannot race into a reconnect. NOTIFY is fire-and-forget, so events emitted during an outage are lost and cannot be replayed. The reconnect logs the gap and names POST /maintenance/integrity-check rather than reporting a clean recovery. Reconciling automatically is left out on purpose: deriving the tenant for a changed page is subtle here, and getting it wrong writes into the wrong user's namespace. Verified against the real database by terminating the listener's backend with pg_terminate_backend. Old code: running=True with is_closed()=True, dead forever. New code: reconnects on its own onto a new server pid. The same probe was run against both implementations so the check is known to discriminate. One existing test mocked the connection with a bare AsyncMock, which models asyncpg's synchronous is_closed() as a coroutine — always truthy, so the connection read as closed once running started deriving from it. Corrected. Co-Authored-By: Claude <noreply@anthropic.com>
Library Desk - API Coordination Service
FastAPI service that coordinates all Library operations
Overview
Library Desk is the central coordination layer for The Library system, providing a unified API for:
- HybridRAG Queries - Combines Neo4j (structure) + Qdrant (semantics) + SearXNG (web)
- Document Ingestion - Parse, chunk, embed, and index documents
- Entity Extraction - NLP to identify classes, functions, concepts
- Relationship Mapping - Link entities in Neo4j knowledge graph
- Mind Map Generation - Query Neo4j graph → Render D3.js visualizations
- Wiki.js Proxy - CRUD operations for dossiers
- Deduplication - Vector similarity + graph analysis
Architecture
Library Desk API (FastAPI)
├── Neo4j (knowledge graph)
├── Qdrant (vector search)
├── Wiki.js (wiki operations)
├── SearXNG (web search)
├── Ollama (embeddings)
└── Redis (caching)
Requirements
- Python: 3.12+
- Dependencies: See
requirements.txt
Configuration
Environment variables (set in Portainer stack):
# Required
LIBRARY_API_KEY=<generate-with-openssl-rand-hex-32>
NEO4J_PASSWORD=<neo4j-password>
WIKIJS_API_KEY=<from-wiki-admin-panel>
# Optional (defaults provided)
NEO4J_URI=bolt://neo4j:7687
NEO4J_USER=neo4j
QDRANT_HOST=qdrant
QDRANT_PORT=6333
WIKIJS_URL=http://wiki:3000
SEARXNG_URL=http://searxng:8080
OLLAMA_URL=http://ollama:11434
OLLAMA_MODEL=mistral-nemo-large:latest
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
REDIS_HOST=redis-shared
REDIS_PORT=6379
REDIS_DB=2
API Endpoints
System
GET /- Root endpointGET /health- Health check (public)GET /stats- System statistics (authenticated)
Query (All require API key)
POST /query/hybrid- HybridRAG (graph + vector + web)POST /query/semantic- Vector search onlyPOST /query/graph- Graph traversal onlyGET /query/related/{id}- Find related content
Content Management (Future)
POST /ingest/document- Index new documentPOST /ingest/wiki-page- Sync Wiki.js pagePOST /wiki/dossier- Create dossier (proxies to Wiki.js)PUT /wiki/dossier/{id}- Update dossierDELETE /wiki/dossier/{id}- Delete dossier
Graph Operations (Future)
GET /graph/entities- List entitiesGET /graph/mindmap/{id}- Generate mind mapPOST /graph/query- Execute Cypher query
Deduplication (Future)
POST /deduplicate/find- Find duplicatesPOST /deduplicate/merge- Merge duplicates
Development
Local Setup
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run development server
uvicorn src.main:app --reload --host 0.0.0.0 --port 8089
Project Structure
Following FastAPI Best Practices:
library-desk/
├── src/
│ ├── __init__.py # Package initialization
│ ├── main.py # FastAPI application
│ ├── config.py # Pydantic settings
│ └── [future modules] # Domain-specific modules
├── requirements.txt # Python dependencies
└── README.md # This file
Future structure (as features are added):
library-desk/
├── src/
│ ├── query/ # Query domain
│ │ ├── router.py
│ │ ├── schemas.py
│ │ ├── service.py
│ │ └── dependencies.py
│ ├── graph/ # Graph domain
│ ├── wiki/ # Wiki domain
│ └── ingest/ # Ingestion domain
Best Practices Implemented
✅ Async routes for I/O operations
✅ Dependency injection for configuration and auth
✅ Pydantic models for request/response validation
✅ Modular settings using Pydantic Settings
✅ API key authentication with Bearer tokens
✅ OpenAPI documentation auto-generated
✅ Proper logging with structured format
✅ CORS middleware configured
✅ Health checks for monitoring
✅ Minor version locking (~=) in requirements
✅ CVE-checked dependencies (Dec 2025)
API Documentation
Once running, access:
- Interactive docs: http://localhost:8089/docs
- ReDoc: http://localhost:8089/redoc
- OpenAPI spec: http://localhost:8089/openapi.json
Authentication
All protected endpoints require a Bearer token:
curl -H "Authorization: Bearer ${LIBRARY_API_KEY}" \
http://localhost:8089/stats
Testing
# Run tests (when implemented)
pytest
# With coverage
pytest --cov=src --cov-report=term
Deployment
Deployed via Portainer stack: /stacks/library-desk.yml
The container:
- Runs on port 8089
- Auto-creates venv on startup
- Installs dependencies from requirements.txt
- Starts uvicorn with 2 workers
- Mounts source code for live editing
Monitoring
- Uptime Kuma: Monitor
/healthendpoint - Logs:
docker logs library-desk - Stats:
GET /stats(requires API key)
Scheduler Integration
See LIBRARIAN_INTEGRATION.md for details on how The Scheduler (Librarian) integrates with Library Desk for automated documentation indexing.
Key Workflow:
- Scheduler mirrors docs to Gitea (daily 03:00)
- Scheduler syncs to Library Desk (daily 03:30)
- Library Desk ingests, chunks, embeds, and indexes
- Content becomes searchable via HybridRAG
Future Enhancements
- Implement HybridRAG query logic
- Add Neo4j connection pooling
- Add Qdrant client initialization
- Implement Wiki.js API proxy
- Add entity extraction (spaCy/NLP)
- Implement mind map generation
- Add deduplication logic
- Implement Scheduler integration endpoints (see LIBRARIAN_INTEGRATION.md)
- Add comprehensive tests
- Add rate limiting
- Add request tracing
References
- FastAPI Best Practices
- FastAPI Documentation
- Pydantic Documentation
- Neo4j Python Driver
- Qdrant Python Client
License
Part of Portainer Core infrastructure.
Support
- Check logs:
docker logs library-desk - Health check:
curl http://localhost:8089/health - API docs: http://localhost:8089/docs