# Operations

## Default deployment mode

The container is safe to deploy before cutover. It starts as a non-root,
read-only operational service with all writer and scheduler flags disabled.

```bash
docker build -t osm-lead-source-service .
docker run --rm --name osm-lead-source-service \
  -p 8080:8080 \
  osm-lead-source-service
```

Available endpoints:

- `GET /` — operator dashboard
- `GET /healthz`
- `GET /readyz`
- `GET /status`
- `GET /api/status`
- `GET /api/dashboard`

There is no HTTP mutation endpoint. A normal default deployment reports mode
`shadow-read-only`. Unknown routes return a JSON 404 and all mutation methods
remain unavailable.

Set the deployed commit as bounded non-secret metadata when desired:

```bash
export OSM_LEAD_SOURCE_RELEASE_SHA=FULL_GIT_COMMIT_SHA
```

## Dashboard evidence snapshot

The dashboard shows the accepted ACT release evidence by default. A later
persisted run may provide a bounded snapshot file:

```bash
export OSM_LEAD_SOURCE_DASHBOARD_SNAPSHOT_PATH=/data/reports/dashboard.json
```

The snapshot must be a regular non-symlink UTF-8 JSON file of at most 512 KiB
and match the exact schema in [Dashboard](DASHBOARD.md). Invalid snapshots fail
back to accepted release evidence with a generic warning. File content, parser
errors, database URLs, and tokens are never returned.

## Generate a shadow report

Prepare checksum and size evidence on a Linux host:

```bash
PBF=/data/extracts/my-small-region.osm.pbf
SHA256=$(sha256sum "$PBF" | cut -d' ' -f1)
SIZE=$(stat -c %s "$PBF")
```

Run the bounded local workflow:

```bash
osm-lead-source shadow-run \
  --pbf "$PBF" \
  --sha256 "$SHA256" \
  --size-bytes "$SIZE" \
  --region my-small-region \
  --profile luxeillum \
  --profile-version 0.1.0-draft \
  --country-code AU \
  --calling-code 61 \
  --output reports/my-small-region-luxeillum.json \
  --max-file-size-bytes 67108864 \
  --max-objects 1000000 \
  --max-total-tags 5000000 \
  --max-blob-size-bytes 33554432 \
  --max-uncompressed-blob-bytes 33554432 \
  --max-total-uncompressed-bytes 268435456 \
  --max-blob-count 4096 \
  --max-leads 500000 \
  --max-review-items 100000
```

The report hash is embedded in the JSON document and must be independently
recorded before approval.

## Run one configured market cycle

`market-run-once` is the guarded foreground entry point for controlled subset and
first full-market runs. It uses the same worker configuration and implementation
as the monthly scheduler, but runs exactly once and never changes scheduler
state.

First configure persistent paths and a bounded market selection:

```bash
export OSM_LEAD_SOURCE_READ_ONLY=true
export OSM_LEAD_SOURCE_NOCO_WRITE_ENABLED=false
export OSM_LEAD_SOURCE_WRITER_ENABLED=false
export OSM_LEAD_SOURCE_STALE_DELETION_ENABLED=false
export OSM_LEAD_SOURCE_SCHEDULER_APPROVED=false
export OSM_LEAD_SOURCE_RUN_ON_START=false
export OSM_LEAD_SOURCE_COUNTRIES=IE,NZ,SG
export OSM_LEAD_SOURCE_CACHE_DIR=/var/lib/osm-lead-source/cache
export OSM_LEAD_SOURCE_REPORT_DIR=/var/lib/osm-lead-source/reports
export OSM_LEAD_SOURCE_DASHBOARD_SNAPSHOT_PATH=/var/lib/osm-lead-source/dashboard.json
```

Preview configuration without network access, downloads, or report writes:

```bash
osm-lead-source market-run-once
```

After verifying the preview and durable mounts, execute one cycle:

```bash
osm-lead-source market-run-once --execute
```

The command refuses writer, Noco-write, and stale-deletion capability flags. It
does not require or enable the scheduler, does not start a background thread,
and has no HTTP or MCP mutation surface. Preserve the emitted shard/cycle JSON,
per-shard reports, acquisition evidence, dashboard snapshot, and content hashes.

## Provision and migrate the production sidecar

Use a dedicated PostgreSQL/PostGIS database. The database name must be exactly
`osm_lead_source`; the host cannot be a test or loopback host. Never point this
configuration at the NocoDB database.

```bash
export OSM_LEAD_SOURCE_MIGRATION_ENV=production
export OSM_LEAD_SOURCE_PRODUCTION_MIGRATION_APPROVAL=apply-osm-lead-source-sidecar-v2
export OSM_LEAD_SOURCE_PRODUCTION_DATABASE_URL='postgresql+psycopg://SIDE_CAR_USER:SIDE_CAR_PASSWORD@osm-sidecar-db:5432/osm_lead_source'
python -m alembic upgrade head
unset OSM_LEAD_SOURCE_PRODUCTION_DATABASE_URL
unset OSM_LEAD_SOURCE_PRODUCTION_MIGRATION_APPROVAL
unset OSM_LEAD_SOURCE_MIGRATION_ENV
```

Generic variables such as `DATABASE_URL`, `POSTGRES_URL`, and
`NOCODB_DATABASE_URL` are ignored. The URL and password are excluded from
configuration representations and validation errors.

After the sidecar is attached to the application, `/status` reports only
`sidecar_configured: true`; it never returns the URL.

## Cutover approval

An insert run needs a short-lived approval row created only after all of the
following evidence exists:

1. The legacy writer has completed and is disabled.
2. The shadow report and profile version are approved.
3. The target Noco table ID is confirmed.
4. The insert cap is intentionally selected.

Insert an approval using a parameterized database client. The values below are
a template, not production evidence:

```sql
INSERT INTO osm_lead_source.writer_cutover_approval (
    approval_id,
    report_hash,
    region_id,
    profile_id,
    profile_version,
    noco_table_id,
    approved_max_inserts,
    legacy_writer_disabled_at,
    approved_by,
    expires_at,
    evidence_json
) VALUES (
    :approval_id,
    :report_hash,
    :region_id,
    :profile_id,
    :profile_version,
    :noco_table_id,
    :approved_max_inserts,
    :legacy_writer_disabled_at,
    :approved_by,
    :expires_at,
    CAST(:evidence_json AS jsonb)
);
```

Approvals are exact, expiring, and revocable. A mismatch in report hash, region,
profile, profile version, table, or insert cap fails before any Noco request.

## Execute an approved insert-only run

Secrets are read only when `insert-run` is executed:

```bash
export OSM_LEAD_SOURCE_WRITER_ENABLED=true
export OSM_LEAD_SOURCE_LEGACY_WRITER_DISABLED=true
export OSM_LEAD_SOURCE_SIDECAR_DATABASE_URL='postgresql://SIDE_CAR_USER:SIDE_CAR_PASSWORD@osm-sidecar-db:5432/osm_lead_source'
export OSM_LEAD_SOURCE_NOCO_BASE_URL='https://nocodb.luxeillum.com'
export OSM_LEAD_SOURCE_NOCO_API_TOKEN='REDACTED'

osm-lead-source insert-run \
  --report reports/my-small-region-luxeillum.json \
  --report-hash REPORT_SHA256 \
  --approval-id APPROVAL_ID \
  --noco-table-id mswfxq541pe6khe \
  --run-id pbf:my-small-region:RUN_ID \
  --run-timestamp 2026-07-16T10:00:00+00:00 \
  --max-report-bytes 67108864 \
  --max-candidates 100000 \
  --max-inserts 100 \
  --execute
```

For every candidate the service first reserves a durable idempotency key, then
performs bounded final duplicate lookups, and only then issues one API POST.
Duplicate evidence is recorded and the POST is skipped.

## Ambiguous POST recovery

A POST is never blindly retried. If the connection fails after POST begins, the
ledger remains in `claimed` state because Noco may already have committed the
row. Keep the writer disabled and verify the Noco record using OSM type, OSM ID,
OSM URL, name, website, phone, and address evidence. Resolve the ledger only
after the external result is known. Automatic recovery tooling is intentionally
not enabled.

## Immediate rollback

Set both flags to false or remove them, revoke the active approval, and stop any
CLI run. The default container remains healthy in read-only mode:

```sql
UPDATE osm_lead_source.writer_cutover_approval
SET revoked_at = CURRENT_TIMESTAMP,
    revoked_reason = :reason
WHERE approval_id = :approval_id
  AND revoked_at IS NULL;
```

No update or delete executor exists. The legacy application must not be removed
until a complete pilot, adoption verification, and rollback review are accepted.
