# Notifier Guardrails And Failure Protection

This app has multiple runtime guardrails. Preserve them during migration.

## Scheduler Lock

`scheduler.php` uses `SchedulerService` with a lock file, currently `/tmp/notifier.lock`.

Purpose:

- Prevent overlapping cron runs.
- Detect stale locks.
- Remove stale locks if the original PID is dead.
- Force-remove very stale locks after twice the configured timeout.

Current config:

- `lock_file`: `/tmp/notifier.lock`
- `lock_timeout`: 300 seconds

If cron runs every minute, this lock is important. Do not remove it from config.

## Stuck Job Reset

Before sending, the scheduler resets jobs stuck in `sending` for more than 10 minutes:

- status becomes `retrying`
- `next_attempt_at` is moved to one minute later
- `last_error` is set to `Reset: stuck in sending state`

This protects against PHP crashes or killed processes during Telegram sends.

## Retry And Backoff

Failed jobs are not dropped immediately.

Backoff schedule:

- attempt 1: 1 minute
- attempt 2: 5 minutes
- attempt 3: 15 minutes
- attempt 4: 60 minutes
- attempt 5: 240 minutes
- later attempts: 480 minutes
- max attempts: 20

After max attempts, the job becomes `failed`.

## Atomic Claiming

Jobs are claimed with a conditional update from `pending`/`retrying` to `sending`.

Purpose:

- Prevent double-sends if two scheduler processes race.
- Keep state in SQLite as the source of truth.

## Duplicate Prevention

The database has a partial unique index on `send_jobs`:

- `job_type`
- `notification_id`
- `source_id`
- `scheduled_for`

It excludes final states `sent`, `cancelled`, and `failed`. This prevents duplicate active jobs for the same occurrence.

## Cron Health Check

The second cron job runs `scripts/cron-health-check.sh` every 10 minutes.

It checks `scheduler_state`:

- `last_run_at`
- `last_success_at`
- `last_error`
- `last_alert_at`

If the scheduler is stale or has an error, it sends an alert. Current config uses MailerSend, not local mail, because `cron_health.method=mailersend`.

Current alert recipient is configured in `config.php` under `cron_health.email_to`.

## Confirmation Links

Main notifications generate a `confirm_token` in `notification_history`.

Telegram messages include a `Complete task` button pointing to:

```text
{app_url}/confirm/{token}/
```

When confirmed:

- the history row gets `confirmed_at`
- pending follow-up jobs for that occurrence are cancelled
- only that occurrence's follow-ups are cancelled

Migration requirement:

- `app_url` must be correct.
- `/notifier/confirm/{token}/` must route to PHP.

## Follow-Up Guardrail

Follow-ups are stored in `notification_subsequents` and generated only after the main message sends successfully.

If the main message fails:

- placeholder history is deleted
- follow-ups are not created
- the main job is retried with backoff

If a user confirms completion:

- pending follow-ups for that history occurrence are cancelled.

## Upload Path Guardrails

API and scheduler resolve upload paths against `upload_path`.

Protections:

- `realpath()` must resolve the base upload directory.
- relative paths are stripped of leading slash.
- paths containing `..` are rejected.
- API validates referenced images exist and are non-empty before notification save.

Migration requirement:

- Keep `upload_path=/workspace/notifier/uploads`, or update it consistently.
- Preserve uploads directory structure.

## Message Formatting Guardrails

Telegram sends use MarkdownV2.

The formatter preserves intentional:

- `*bold*`
- `_italic_`
- `~strikethrough~`
- inline code
- code blocks

It escapes other Telegram MarkdownV2 characters to reduce API failures.

## Recurrence Safety

Recurrence calculations:

- validate recurrence type and fields
- calculate in notification timezone
- store UTC in SQLite
- cap internal recurrence iteration at 1000 to avoid infinite loops
- support weekend adjustment only for `monthly_date`

Migration requirement:

- server timezone can differ, but PHP timezone data must exist.
- keep `display_timezone=America/Mexico_City` unless intentionally changing behavior.

## Pause/Edit/Delete Safety

- Pausing a notification cancels pending jobs.
- Editing a notification deletes old schedule rows, reinserts new rules, and cancels pending jobs so the scheduler recreates them.
- Deleting a notification cascades related DB rows.

This means DB foreign keys and schema indexes must be intact.
