# Add API Account to Master

The authenticated page is available at `/master-api` from **Add API Account to Master** in the existing sidebar. Its styles and scripts apply only to this feature.

## Configure the connection

Complete `storage/app/master-api/connection.ini`:

```ini
[ssh]
host = "master-server.example"
port = 22
username = "ssh-user"
password = "ssh-password"
host_key_sha256 = ""

[database]
host = "127.0.0.1"
port = 3306
name = "master-database"
username = "database-user"
password = "database-password"

[runtime]
python = "python3"
```

The database host is relative to the SSH server. The public database port can stay closed. Use the Python interpreter that already runs your original import scripts; it must have `mysql-connector-python` installed. Both destination tables must use InnoDB. The SSH user needs permission to run Python, and the database user needs SELECT, INSERT, UPDATE and DELETE access to the two masterAPP tables. Enter valid destination server IDs; the fixed owner is `user_id = 1`.

The connection file is outside `public`, ignored by version control, and should stay readable only by the web server user (`www-data`, file mode `600`). Its directory should be owned by `www-data` with mode `700`. Run feature-related Artisan commands as `www-data` to avoid root-owned compiled views.

Use **Test connection** after configuring it. Passwords and account tokens are sent through encrypted SSH stdin, never shell arguments or remote files. The worker runs in memory without installing files or changing masterAPP application code. The SSH host key is pinned on first connection; alternatively provide its independently verified `SHA256:…` fingerprint in `host_key_sha256`. If the server key changes, verify it before updating the pin.

## Import behavior

1. Select domains using the source `projects.domain` column and check availability.
2. Eligible accounts have nonempty access/refresh tokens, usable Google web credentials, and no revoked marker. This is a database check, not a live Google token check. Repeated emails select the newest eligible source account. Counts are unique within the selected domains; per-domain counts may overlap when an email exists in both domains.
3. Pick randomly or by ascending account ID, choose 1–20,000 accounts, and enter up to 500 unique positive server IDs, one per line. With **Smallest ID first**, **Start position** is the 1-based position in the eligible, deduplicated account list for the selected domains, not a database ID. Position 501 with quantity 300 selects positions 501–800. If fewer accounts remain, all remaining accounts are picked: with 2,000 eligible accounts, position 1,700 with quantity 500 selects 301 accounts (1,700–2,000 inclusive). Positions remain stable for the same domain selection until source data or eligibility changes. The preview stores the exact selected IDs, so execution does not recalculate the offset.
4. **Add & update** matches destination projects by `project_name` and accounts by trimmed, case-insensitive email. It refreshes existing project credentials and selected account tokens without inserting another matching account. Existing names, creation times, and token types are preserved. As in the original Python script, newly inserted accounts receive a six-letter token type and null expiry/connection timestamps.
5. **Clear & replace** deletes every row from both destination tables, across every domain, then inserts the selection. The review requires an explicit checkbox confirming the displayed deletion counts.
6. Review the exact selection, changed projects, and final server counts before submitting. Random selection is preserved between review and execution.

Projects remain intact: no credential duplication and no account splitting within a project. Larger complete projects are assigned first to the server with the lowest account count. Add mode includes existing accounts belonging to affected projects and respects existing loads from untouched projects. The review displays those final totals. Server counts may differ when projects have different sizes.

**Update server IDs** reassigns every existing masterAPP project across the entered IDs. It changes project `server_id` and `updated_at` only; it does not alter account rows or Google credentials.

## Transactions and concurrent changes

All writes for one operation run in one database transaction. A failed insert, update or delete rolls back the operation, including replacement deletions. Foreign keys remain enabled. Duplicate emails/project names already present in masterAPP are reported before importing; the feature does not silently remove existing duplicates.

Reviews expire after 15 minutes, are tied to the signed-in user and hostname, and store source IDs and fingerprints without credentials or tokens. A source token change, connection-setting change, or destination account/project-assignment change requires a fresh review. Changes to token timestamps from masterAPP's refresh job do not invalidate the destination review. Execution serializes feature writes with a database advisory lock and locks destination rows during the transaction.

Repeat submissions of a successful review return the saved result. A submitted operation with an interrupted connection cannot be submitted again using the same review. If the SSH connection drops around commit, check masterAPP's state before creating a new review; the transaction may already have committed.

## Validation

Run from the project directory:

```sh
runuser -u www-data -- php vendor/bin/phpunit tests/Feature/MasterApi --do-not-cache-result
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests/master-api -v
```

PHP tests use an isolated in-memory SQLite source and mocked SSH responses. Python tests exercise balancing and parameterized writes against an isolated SQLite schema, including rollback after a replacement failure. These tests do not connect to or mutate the real masterAPP database. A live connection check is still needed after the INI file is completed.
