# Deployment on WHM/cPanel and AlmaLinux 8

This procedure installs Wilhelm Calendar Control Center at `https://calendar.thewilhelms.com` without Docker.

Use a normal cPanel account for the application files, database, and cron. Root access is useful for checking Apache, PHP packages, and AutoSSL, but the web application should not run from `/root` or be owned by root after deployment.

## 1. Create the hostname and document root

In cPanel, open Domains and create `calendar.thewilhelms.com` with this document root:

```text
/home/CPANEL_USER/calendar-control-center/public
```

Replace `CPANEL_USER` with the cPanel account name. The project itself remains outside `public_html`; only its `public` directory is web-accessible.

In MultiPHP Manager, assign PHP 8.3 to `calendar.thewilhelms.com`. PHP 8.2 is also supported.

Verify the available cPanel PHP binaries over SSH:

```bash
ls -1 /opt/cpanel/ea-php*/root/usr/bin/php
/opt/cpanel/ea-php83/root/usr/bin/php -v
```

Use the installed path that reports PHP 8.2 or newer. The examples below use PHP 8.3:

```bash
export PHP_BIN=/opt/cpanel/ea-php83/root/usr/bin/php
export PATH="$(dirname "$PHP_BIN"):$PATH"
php -v
```

Do not use the server's default PHP 8.1 CLI for this application.

## 2. Upload and extract the release

Upload the release ZIP to the cPanel user's home directory, then extract it:

```bash
cd /home/CPANEL_USER
unzip wilhelm-calendar-control-center-1.0.0.zip
mv wilhelm-calendar-control-center-1.0.0 calendar-control-center
cd calendar-control-center
```

Confirm ownership. Replace `CPANEL_USER` and `CPANEL_GROUP` as appropriate:

```bash
chown -R CPANEL_USER:CPANEL_GROUP /home/CPANEL_USER/calendar-control-center
```

Run the included preflight check with the selected EA-PHP binary:

```bash
"$PHP_BIN" bin/preflight.php
```

## 3. Install Composer dependencies

Confirm Composer is available while the EA-PHP directory is first in `PATH`:

```bash
command -v composer
composer --version
php -v
```

Install production dependencies:

```bash
composer install --no-dev --optimize-autoloader --no-interaction
```

For a test-enabled staging installation, omit `--no-dev`, run the test suite, and then reinstall with `--no-dev` for production:

```bash
composer install --no-interaction
php tests/smoke.php
vendor/bin/phpunit
composer install --no-dev --optimize-autoloader --no-interaction
```

## 4. Create the MariaDB database

Use cPanel's MySQL Database Wizard to create:

- A database, such as `CPANEL_USER_calendar`
- A dedicated user, such as `CPANEL_USER_calendar`
- A long random database password
- All privileges for that user on only this database

Do not reuse a WordPress or other application database account.

## 5. Create the environment file and application key

```bash
cp .env.example .env
chmod 600 .env
"$PHP_BIN" bin/generate-key.php
```

Copy the generated `base64:` value into `APP_KEY` in `.env`. Configure at least these values:

```dotenv
APP_ENV=production
APP_DEBUG=false
APP_URL=https://calendar.thewilhelms.com
APP_TIMEZONE=America/New_York
APP_KEY=base64:REPLACE_WITH_GENERATED_VALUE

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=CPANEL_USER_calendar
DB_USERNAME=CPANEL_USER_calendar
DB_PASSWORD=REPLACE_WITH_DATABASE_PASSWORD

MAILER_DSN=sendmail://default
MAIL_FROM_ADDRESS=calendar@thewilhelms.com
MAIL_FROM_NAME="Calendar Control Center"
```

Keep `ALLOW_PRIVATE_ICS_HOSTS=false`. The current calendar sources are public HTTPS endpoints and do not require access to private network addresses.

The `APP_KEY` encrypts feed URLs, bridge tokens, and any future encrypted settings. Back it up together with the database. Losing it makes encrypted values unrecoverable. Changing it without re-encrypting stored data will break decryption.

## 6. Set filesystem permissions

```bash
find storage -type d -exec chmod 700 {} \;
find storage -type f -exec chmod 600 {} \;
chmod 700 bin/console bin/cron.php bin/backup.php bin/generate-key.php bin/preflight.php
chmod 600 .env
```

The cPanel account should own the project. PHP-FPM normally executes as that account and will be able to write to `storage`.

## 7. Create the schema and administrator

```bash
"$PHP_BIN" bin/console migrate
"$PHP_BIN" bin/console user:create scott@thewilhelms.com
"$PHP_BIN" bin/console status
```

The password prompt requires at least 14 characters and does not echo the password.

## 8. Enable HTTPS

Run AutoSSL for the cPanel account in WHM or cPanel. Confirm that the certificate covers `calendar.thewilhelms.com` before signing in.

Verify:

```bash
curl -fsS https://calendar.thewilhelms.com/healthz
```

Expected output resembles:

```json
{"status":"ok","version":"1.0.0","time":"2026-08-04T12:00:00+00:00"}
```

Do not enable a long-lived HSTS policy until HTTPS, renewal, and the hostname are confirmed.

## 9. Configure the one-minute scheduler

In cPanel Cron Jobs, add:

```cron
* * * * * /opt/cpanel/ea-php83/root/usr/bin/php /home/CPANEL_USER/calendar-control-center/bin/cron.php >/dev/null 2>>/home/CPANEL_USER/calendar-control-center/storage/logs/cron-error.log
```

Adjust the PHP path and cPanel username. The script emits nothing on success. Review and rotate `storage/logs/cron-error.log` through the server's normal log-management process.

Test the scheduler manually:

```bash
"$PHP_BIN" bin/console scheduler:run
```

## 10. Configure email

The default `sendmail://default` transport uses the server's local mail transfer agent. In Settings, use `scott@thewilhelms.com` as the notification destination and send a test notification.

Verify that SPF, DKIM, and DMARC are configured for the sending domain and that the message does not land in spam. Symfony Mailer also supports an authenticated SMTP DSN if local delivery is unreliable.

## 11. Rotate and add calendar feeds

Every private ICS URL or token previously pasted into a chat should be regenerated before production use.

After rotation:

1. Sign in to Calendar Control Center.
2. Open Calendar Sources.
3. Add one source at a time.
4. Use Test Feed before saving when possible.
5. Set the source identity email so RSVP status can be matched.
6. Keep simulation mode enabled while reviewing events and conflicts.
7. Follow `docs/SOURCE-PLAN.md` for recommended policies.

The personal and family URLs previously supplied were identical. The duplicate-feed guard will reject the second copy. Configure that as one combined source until a genuinely separate family-calendar feed exists.

## 12. Configure notifications

The supplied defaults are:

- Morning agenda: 6:00 AM
- Evening preview: 9:00 PM
- Weekly review: Sunday at 6:00 PM
- Quiet hours: 9:00 PM through 6:00 AM
- Pending invitation review: every four hours
- Conflict lookahead: 90 days
- Repeat unresolved conflicts daily: enabled

Scheduled digests are delivered at their configured times. Ordinary alerts are held until quiet hours end unless they qualify for the configured urgent bypass.

Email is the initial urgent channel. A generic HTTPS webhook can be added later by setting `URGENT_WEBHOOK_URL` and enabling Webhook under Settings.

## 13. Configure private Busy mirrors

Read `docs/GOOGLE-BRIDGE.md`. Create one mirror target for each Google account that should receive Busy copies. Keep both the application and each Apps Script bridge in simulation mode until the preview is correct.

## 14. Post-install verification

Run:

```bash
"$PHP_BIN" bin/preflight.php
"$PHP_BIN" tests/smoke.php
"$PHP_BIN" bin/console status
"$PHP_BIN" bin/console scheduler:full
curl -fsS https://calendar.thewilhelms.com/healthz
```

Then verify in the interface:

- Every source shows a recent successful sync.
- No source is unintentionally duplicated.
- The next 14 days of events look correct.
- Known overlaps appear in Conflicts.
- The notification test is delivered.
- Mirror previews contain only category-based Busy titles.
- No live Busy events are written until simulation is explicitly disabled in the Apps Script.
