# Catalog operations

The supported deployment is one Python process on a workstation, listening on loopback, with a SQLite database on local disk. The operations rehearsal completes the process-failure and recovery checks for that deployment. All records in the rehearsal are synthetic.

## Start and inspect

Python 3.12 or newer is sufficient. Keep the application directory separate from the data directory. On POSIX systems, restrict newly created files before starting:

```sh
umask 077
python app.py --seed --db data/catalog.sqlite --port 8011
```

Use `--seed` only for the supplied example catalog. The fixed local accounts remain demonstration accounts. The server accepts only localhost Host values and same-origin browser requests; it does not provide TLS or an external identity system. Request sockets have a 15-second timeout.

From a second terminal, this checks SQLite pages, foreign keys and table counts without changing catalog contents:

```sh
python operations.py inspect --db data/catalog.sqlite
```

## Back up a running catalog

The snapshot command uses SQLite's online backup API. It includes committed WAL transactions and excludes uncommitted writes. It creates a new folder containing `store.sqlite` and `manifest.json`; it refuses to reuse a destination.

```sh
mkdir -p backups
python operations.py backup --db data/catalog.sqlite --out backups/review-001
```

A snapshot is checked with `integrity_check` and `foreign_key_check`. The manifest records project identity, table counts, file size and SHA-256. The database and manifest receive mode 0600; new snapshot folders receive 0700 on POSIX. Checksums detect accidental corruption, not an attacker who can replace both files.

For a finite rehearsal of the scheduler:

```sh
python backup_schedule.py --db data/catalog.sqlite --out backups --interval 1 --count 2
```

For periodic snapshots during use, omit `--count` and choose an interval in seconds. Ctrl+C stops it. Run it under your process supervisor if it needs to survive a terminal closing. It exits nonzero on a failed snapshot and retains every snapshot; it does not silently remove old history. Copy snapshots to a separately protected device or storage location if a single disk loss is in scope. That copy is outside this local package.

## Restore without overwriting the working store

Stop the app before switching stores. The restore target must not exist.

```sh
python operations.py restore --snapshot backups/review-001 --out recovered-catalog
python app.py --db recovered-catalog/store.sqlite --port 8011
```

Sign in again. Sessions are in memory and intentionally do not survive a restart. Compare the latest applied supplier versions and reports before resuming imports. Retain the old database until the restored catalog has been reviewed.

Restore checks the manifest, checksum, schema tables, page integrity, foreign keys and table counts, then copies the database to a new directory. A bad snapshot does not create the destination. Restoring an older snapshot also restores older catalog versions: preserve the original CSVs and replay only the batches received after that snapshot, reviewing their current diffs.

## Executed failure cases

Run `python operations_rehearsal.py`. Results are in `operations-report.html`, `completion.json`, and `results/operations-checks.csv`.

- A child process exits immediately after approval writes but before commit. All changed items, the supplier version, batch state and audit entry roll back together.
- A child commits and exits before returning the result. Retrying the batch returns the same application, with one version advance and one applied audit event.
- Four writers import eight suppliers with 250 rows each. All 2,000 rows remain associated with the correct supplier and each version advances once.
- A snapshot taken while a WAL writer holds an uncommitted insert excludes that insert. The restored catalog matches every supplier, item, batch and audit row.
- Corrupted snapshots and existing restore destinations are rejected. A two-iteration scheduler run creates checked snapshots and exits.

An expected rollback or rejection is a passing verification outcome. It is not a claim that the injected failure did not occur.

## Capacity and recovery calculation

The earlier size sweep measures 1,000, 10,000 and 100,000-row imports in separate processes. The new rehearsal measures four concurrent writers. Both runs describe this execution host. The parser and diff remain in memory, so a deployment should keep the documented 16 MiB and 100,000-row limits rather than extrapolate unlimited throughput from the timings.

For a nominal 900-second snapshot interval, a successful snapshot and separate backup storage, the modeled recovery-point gap is at most 900 seconds plus snapshot duration. `completion.json` calculates that value from the observed snapshot duration. Actual recovery time also includes discovering the failure, preparing storage, operator review and restarting the app; the measured restore time only covers validation and copying.

For a larger hosted design, the same review invariant maps to a supplier-version row locked during approval, with immutable source objects stored separately from the database. The local tests establish all-or-nothing application and stale-review rejection under SQLite's serialized writers. They do not establish PostgreSQL lock behavior, external authentication or a hosted availability target. Those are different deployment assumptions, not hidden results in this package.
