# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

This is a monorepo of four **independent** Python utilities for operating Neo4j databases (self-hosted clusters and Neo4j Aura). Each top-level directory is its own deployable with its own dependencies, Docker image, tooling, and Python version — there is **no shared code** and the root `pyproject.toml` is an empty stub. Treat each subdirectory as a separate project: `cd` into it before running any commands.

| Dir | Purpose | Runs as | Python | Tooling |
|-----|---------|---------|--------|---------|
| `backup/` | `neo4j-admin` backup of a cluster → S3 (KMS-encrypted, via `s5cmd`), optional QA refresh | EC2 cron (Docker) | 3.13 | `uv` + `ruff` |
| `restore/` | Promotes latest restored `graph.*` DB by repointing the `graph.db` alias + health checks; runs on every cluster node | On-cluster | 3.8 | `venv` + `flake8` + `pytest` |
| `refresh_aura/` | Refreshes a Neo4j Aura instance by overwriting it from another (source→dest) via the Aura API | Docker/Jenkins | 3.11 | `venv` + `flake8` |
| `users/` | CLI to create/delete/rename Neo4j users, manage roles, backup user/role files to S3 over SFTP | CLI | (venv) | `flake8` |

## Commands

Always `cd` into the relevant subdirectory first — tooling differs per project.

**backup/** (uv + ruff):
```bash
cd backup
uv sync --locked
./scripts/unit-lint.sh          # ruff check + ruff format --check + typos
uv run ruff check config.py index.py refresh_neo4j.py
uv run index.py                 # entrypoint (backup + upload)
uv run refresh_neo4j.py <prefix> --apply   # typer CLI: refresh QA from S3 backup
```

**restore/** and **refresh_aura/** (Makefile: venv + flake8):
```bash
cd restore        # or refresh_aura
make env          # create venv, install requirements + dev deps
make lint         # flake8 (restore: also builds venv + runs tests first)
make test         # pytest tests/unit  (restore only)
venv/bin/python3 -m pytest tests/unit/test_index.py                    # single file
venv/bin/python3 -m pytest tests/unit/test_index.py::test_name        # single test
```

**refresh_aura/** also runs via docker-compose (see `refresh_aura/README.md` — requires `awsume dev` + ECR login):
```bash
cd refresh_aura && make run_refresh    # docker compose up run-refresh
```

**users/** (no Makefile):
```bash
cd users
python manage_neo4j_users.py -a create -u <username> -r <role>   # actions: create|delete|rename|resetpassword|addrole
```

## Architecture & conventions

**Config-as-module pattern.** Every project has a `config.py` that is imported for its side effects at module load: it reads env vars (via `python-dotenv`'s `load_dotenv()`), pulls secrets from AWS Secrets Manager, and **raises on invalid/missing config at import time**. `backup` and `users` use `secrets_manager.LambdaSecretsManager` (Orchard's wrapper; secret paths follow `${env}/${service_name}/SECRET_NAME` — change `SERVICE_NAME` to relocate secrets). Because import triggers AWS calls and validation, importing these modules (including in tests) requires valid AWS creds / env unless mocked.

**Observability is (mostly) uniform.** All entrypoints set up `owslogger.logger.setup(...)` with the same `config.*` fields and emit DataDog count metrics (`call_datadog_with_metric`) for attempt/success/failure. Most also initialize `sentry_sdk` — **except `backup/`, where Sentry is a no-op** (no `SENTRY_DSN` in config, no `sentry_sdk.init()` call), so its `capture_exception` calls report nothing. Follow the full pattern (including a working Sentry init) when adding an operation.

**Neo4j `graph.*` database + alias model (backup & restore).** The live database is reached through an alias named `graph.db` that points at a timestamped `graph.<...>` database. Restore/refresh logic:
- Never drops `graph.db` (the alias name) or the current alias target, or `system`.
- Selects the "latest" candidate lexicographically (names share a fixed timestamp format) — see `restore/refresh_manager.py` and `backup/refresh_neo4j.py`.
- Repoints the alias with `CREATE OR REPLACE ALIAS`, then re-applies post-alias config/grants (txLogEnrichment, architect CDC grants) and ensures the alias target is also the default database.
- All identifiers are backtick-escaped (` `` `) and URLs single-quote-escaped before interpolation into Cypher — preserve this when editing query builders.

**Neo4j 4 vs 5 branching.** `backup/` supports both major versions via `NEO4J_SERVER_MAJOR_VERSION` (4 or 5), which selects entirely different `neo4j-admin` command shapes and S3 upload globs. Any change to backup commands must update both branches. The exception is `inspect_backup_metadata` (which collects the backup's highest transaction id and publishes a `backup-metadata.json` sidecar to S3 for Aura import): it is **v5-only and best-effort** — skipped with a log on v4, and never fails the backup on error — so it deliberately does not add a v4 command branch.

**DRY_RUN.** `backup` (`config.DRY_RUN`) and `restore` (`config.NEO4J_DRY_RUN`) gate all mutating operations behind a dry-run flag that logs the intended command/query instead of executing. Keep new mutations behind this flag.

**Retry/polling idiom.** Long async operations (Aura overwrite, cluster health, DB coming online) use bounded retry loops with `time.time()`-based timeouts and randomized sleeps (`secrets.SystemRandom()` / `random.randint`) to avoid thundering-herd restarts. Aura calls also re-fetch the OAuth token on `TokenExpired` because refreshes can outlast the token TTL.

## Backup deployment (cross-repo)

The `backup/` module is **not** run by hand in prod, and its behavior is only fully understandable by reading two other repos. See `backup/README.md` for detail; the essentials:

- **Terraform** `terraform-infra/prod/neo4j_4/backup.tf` provisions the host as an **EC2 ASG (size 1)** + launch template + Chef bootstrap — *not* Fargate, despite `backup_service_name = neo4j-cluster-backup-fargate` and stale `backup_service_task_*` variables. It also defines the IAM (S3/KMS/secrets/ECR), security group, and AMI.
- **Chef** `chef-repo/cookbooks/irrigate-neo4j`, recipe `recipes/backup.rb`, writes `/usr/local/sbin/neo4j-backup.sh` and a **cron at 04:15** that runs the container detached with `NEO4J_SERVER_MAJOR_VERSION=5`, `NEO4J_BACKUP_FROM_FOLLOWER=false`, and `NEO4J_BACKUP_REFRESH_QA=true`. `neo4j_backup_wrapper.sh.erb` in that cookbook is **stale/unused** — ignore it.
- Because the cron sets `NEO4J_BACKUP_REFRESH_QA=true`, **every successful prod backup automatically drops and recreates `graph.*` databases on the QA cluster** (`refresh_neo4j_backup(apply=True)`).

**Known issues (verified during review, still open) — read `backup/README.md` "Known issues" before changing this module.** In short: (1) Sentry is dead in `backup/`; (2) the cron discards container stdout/stderr, so failures are near-invisible; (3) the DataDog backup monitor lives in `terraform-infra/prod/neo4j/` (v5) and queries the `neo4j_cluster_backup.*` namespace while the code emits `neo4j-cluster-backup-fargate.*` — a likely mismatch, and there is no monitor in `neo4j_4/`; (4) an automated QA-refresh failure masks a successful backup's `backup_success` metric; (5) the IAM S3 policy grants delete across `backup/neo4j/*` (all clusters), broader than this host's own prefix.

## Style

- Line length **120**; single quotes for inline strings, double for docstrings (enforced by both `flake8` config in `.flake8`/`pyproject.toml` and `ruff` in `backup`).
- `backup/` additionally runs `typos` spell-checking in CI lint.

## CI/CD

Root `Jenkinsfile` runs compliance checks, AI PR review (non-master), and SonarQube (master) — it does **not** run the per-project lint/test. Each deployable has its own build (`backup/Dockerfile`, `refresh_aura/Dockerfile`, and per-project `Jenkinsfile`s) that pull from Orchard's ECR (`086679231553.dkr.ecr.us-east-1.amazonaws.com`) and PyPI (`pypi.theorchard.io`). `restore/Jenkinsfile` is an orchestration pipeline chaining downstream Jenkins jobs, not a build.
