jpmschweitzerandClaude 4f09a12171 fix: supervise the Wiki.js change listener so it survives a database restart
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>
2026-08-08 21:37:17 +02:00

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 endpoint
  • GET /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 only
  • POST /query/graph - Graph traversal only
  • GET /query/related/{id} - Find related content

Content Management (Future)

  • POST /ingest/document - Index new document
  • POST /ingest/wiki-page - Sync Wiki.js page
  • POST /wiki/dossier - Create dossier (proxies to Wiki.js)
  • PUT /wiki/dossier/{id} - Update dossier
  • DELETE /wiki/dossier/{id} - Delete dossier

Graph Operations (Future)

  • GET /graph/entities - List entities
  • GET /graph/mindmap/{id} - Generate mind map
  • POST /graph/query - Execute Cypher query

Deduplication (Future)

  • POST /deduplicate/find - Find duplicates
  • POST /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:

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 /health endpoint
  • 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:

  1. Scheduler mirrors docs to Gitea (daily 03:00)
  2. Scheduler syncs to Library Desk (daily 03:30)
  3. Library Desk ingests, chunks, embeds, and indexes
  4. 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

License

Part of Portainer Core infrastructure.

Support

S
Description
The libary desk api in the tower-of-joy project. The toolkit for The Librarian in Tatlock's household
Readme
1.1 MiB
2026-08-16 19:32:18 +02:00
Languages
Python 97.7%
JavaScript 1.5%
Makefile 0.5%
Shell 0.3%