# Go-live runbook — chronis.malamoglou.com (Linode)

First production deploy. Migrates **config-only** data from local → live (no test
prospects/runs), reuses the local `APP_KEY` so encrypted integration secrets
decrypt, and fronts the Docker stack with Apache2 + Let's Encrypt TLS.

> Two transfer artifacts are produced on the LOCAL machine and are git-ignored
> (they contain encrypted secrets): `chronis-config.sql`, `chronis-skills.tgz`.
> Regenerate them anytime with the commands in **Appendix A**.

Placeholders: `SRV` = your SSH login (e.g. `root@chronis.malamoglou.com`),
`APP_DIR` = where the repo is cloned on the server (e.g. `/var/www/chronis-ai-agents`).

---

## 0. Transfer the artifacts (run on the LOCAL machine)

```bash
cd /var/www/chronis-ai-agents
scp chronis-config.sql chronis-skills.tgz SRV:APP_DIR/
```

Everything from here runs **on the server** (`ssh SRV`, then `cd APP_DIR`).

---

## 1. Production `.env`

```bash
cd APP_DIR
[ -f .env ] || cp .env.example .env
```

Edit `.env` and set:

```ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://chronis.malamoglou.com

# REUSE the local key — required for integration secrets to decrypt.
# (Value is in your local .env as APP_KEY=base64:… — copy it verbatim.)
APP_KEY=base64:PASTE_LOCAL_APP_KEY_HERE
APP_ENCRYPTION_KEY_VERSION=1

# Pick a strong DB password for production (used by both app + postgres service).
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=chronis
DB_USERNAME=chronis
DB_PASSWORD=CHOOSE_A_STRONG_PASSWORD

REDIS_HOST=redis
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redis
SESSION_SECURE_COOKIE=true
```

Copy these **secret values from your local `.env`** (same keys, same values):
`OPENROUTER_API_KEY`, `OPENROUTER_BASE_URL`, `OPENROUTER_DEFAULT_MODEL`,
`OPENROUTER_CHEAP_MODEL`, `OPENROUTER_HEAVY_MODEL`, `SLACK_BOT_TOKEN`,
`SLACK_SIGNING_SECRET`, `SLACK_DEFAULT_CHANNEL`, `PIPEDRIVE_API_KEY`,
`MAILBLAZE_API_KEY`, `GMAIL_CLIENT_ID`, `GMAIL_CLIENT_SECRET`.
(Apollo's key is in the DB dump, not `.env`.)

---

## 2. Bring up the stack and migrate (schema only — do NOT seed)

```bash
docker compose build
docker compose up -d postgres redis
sleep 5
docker compose up -d app nginx horizon scheduler

docker compose exec -T app php artisan migrate --force
```

Do **not** run `db:seed` — the config dump in step 3 carries the real rows
(seeding would collide on primary keys).

---

## 3. Load the config data

Truncate the config tables (safe/idempotent on a fresh DB) then load the dump.
`RESTART IDENTITY` resets the ID sequences; the dump's own `setval` lines then
restore the correct next-ID values.

```bash
docker compose exec -T postgres psql -U chronis -d chronis -c \
"TRUNCATE companies,users,agent_types,icps,buyer_personas,agents,skills,agent_skills,skill_versions,sequences,sequence_steps,integrations,integration_secrets,playground_scenarios RESTART IDENTITY CASCADE;"

docker compose exec -T postgres psql -U chronis -d chronis < chronis-config.sql
```

Expect a series of `COPY n` lines and `setval` results, no errors.

---

## 4. Restore the editable skill files

These live in `storage/app/skills` (git-ignored), so they did NOT arrive via git.

```bash
tar -xzf chronis-skills.tgz -C storage/app/
# The PHP container runs as uid 82 — make the files owned by it.
docker compose exec -T app chown -R 82:82 storage/app/skills
docker compose exec -T app ls storage/app/skills/14
```

---

## 5. Cache config + restart workers

Horizon loads config once at boot — always restart it after a deploy/env change.

```bash
docker compose exec -T app php artisan config:cache
docker compose exec -T app php artisan route:cache
docker compose exec -T app php artisan view:cache
docker compose restart horizon scheduler
```

---

## 6. Verify before exposing the domain

```bash
# 1 company / 1 agent / 16 skills present
docker compose exec -T postgres psql -U chronis -d chronis -At -c \
"select 'agents',count(*) from agents union all select 'skills',count(*) from skills union all select 'sequences',count(*) from sequences;"

# Integration secret decrypts under the reused APP_KEY (prints the Mailblaze from_email)
docker compose exec -T app php artisan tinker --execute='
$mb = \App\Models\Integration::withoutGlobalScopes()->where("type","mailblaze")->first();
$c  = app(\App\Services\Secrets\IntegrationSecrets::class)->read($mb);
echo "mailblaze from_email: ".($c["from_email"] ?? "(decrypt FAILED)")."\n";'

# App serves locally (200/302)
curl -sI http://127.0.0.1:8080 | head -1
```

If the decrypt line shows `(decrypt FAILED)`, the `APP_KEY` doesn't match the
one used locally — fix `.env`, `config:cache`, retry. (No data loss; secrets
just won't read until the key matches.)

---

## 7. Apache2 reverse proxy + TLS

The Docker nginx listens on host `127.0.0.1:8080`. Apache fronts it on 80/443.

```bash
sudo a2enmod proxy proxy_http headers

sudo tee /etc/apache2/sites-available/chronis.conf >/dev/null <<'EOF'
<VirtualHost *:80>
    ServerName chronis.malamoglou.com

    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"
    ProxyPass        / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/

    ErrorLog  ${APACHE_LOG_DIR}/chronis-error.log
    CustomLog ${APACHE_LOG_DIR}/chronis-access.log combined
</VirtualHost>
EOF

sudo a2ensite chronis
sudo apache2ctl configtest && sudo systemctl reload apache2

# TLS — certbot adds the :443 vhost and an http→https redirect automatically.
sudo certbot --apache -d chronis.malamoglou.com
```

> Laravel sits behind a proxy. If login redirects or asset URLs come back as
> `http://`, set trusted proxies in `bootstrap/app.php`
> (`->withMiddleware(fn ($m) => $m->trustProxies(at: '*'))`), then `config:cache`.
> The `RequestHeader set X-Forwarded-Proto "https"` above usually makes this
> unnecessary.

---

## 8. Smoke test (the real go-live check)

1. Open `https://chronis.malamoglou.com` → log in as your existing user.
2. Open the agent → confirm ICP, personas, 16 skills, both sequences, 4
   integrations all present.
3. Playground → generate a draft → **Send test** with the real template id
   (`tp451xdvebfed`) to spiro@mailblaze.com → confirm it renders.
4. Leave the agent **paused/draft** until you've eyeballed it; flip to **active**
   only when you're ready for it to source + send for real.

---

## Appendix A — regenerate the transfer artifacts (LOCAL machine)

```bash
cd /var/www/chronis-ai-agents
docker compose exec -T postgres pg_dump -U chronis -d chronis \
  --data-only --no-owner --no-privileges \
  -t companies -t users -t agent_types -t icps -t buyer_personas \
  -t agents -t skills -t agent_skills -t skill_versions \
  -t sequences -t sequence_steps -t integrations -t integration_secrets \
  -t playground_scenarios > chronis-config.sql

tar -czf chronis-skills.tgz -C storage/app skills/14
```

## Appendix B — rollback / re-run

Steps 3–4 are idempotent (truncate + load, extract). To redo config: re-run
step 3. To wipe and start clean: `docker compose down` (keeps the named volume)
or `docker compose down -v` (drops the DB volume entirely), then from step 2.
