# Notifier

A self-hosted Telegram notification scheduler with a simple PHP UI, a JSON API, and a cron-driven delivery engine.

This README is intentionally detailed. It documents what the app does, how it works, and what each file is responsible for so you can deploy and maintain it with confidence.

---

## What the app does (behavioral overview)

- Create notifications that send to Telegram at specific dates/times or on recurring schedules.
- Add follow-up reminders that fire after the main send until the task is confirmed.
- Attach images to main notifications and to individual follow-ups.
- Preview upcoming occurrences for recurring notifications.
- Send a one-off test message to a test chat.
- Confirm completion via a public confirmation link (no login required) that cancels follow-ups for that occurrence only.

---

## High-level architecture (how the pieces fit)

The system has three layers that work together:

1. UI (PHP + Tailwind + JS)
   - Pages under `/notifier/*` call the JSON API via `js/api.js`.
   - `bootstrap.php` enforces auth for UI pages and exposes config to JavaScript.

2. API (JSON over HTTP)
   - `api/index.php` routes all `/api/*` requests.
   - CRUD operations are implemented in `src/services/*`.

3. Scheduler (CLI via cron)
   - `scheduler.php` runs every minute.
   - It creates due jobs, sends messages, creates follow-ups, updates recurrences, and resets stuck jobs.

Notes:
- The scheduler is the only component that sends Telegram messages in production.
- The API is used by the UI and can also be used externally with `X-API-Token`.

---

## Routes (UI + API)

UI routes are handled by `index.php` (with `.htaccess` rewrites or a web server fallback).

UI (base `/notifier`):
- `/login/` -> `login.php`
- `/logout/` -> `logout.php`
- `/dashboard/` -> `dashboard.php`
- `/notifications/` -> `notifications.php`
- `/notifications/new` -> `notification-edit.php`
- `/notifications/edit/{id}` -> `notification-edit.php` (loads `?id=...`)
- `/categories/` -> `categories.php`
- `/history/` -> `history.php`
- `/console/` -> `console.php`
- `/confirm/{token}` -> `confirm.php` (public, no auth)

API (base `/notifier/api`):
- All JSON routes are handled by `api/index.php`.

---

## Core flows (end-to-end)

### 1) Create a notification
- UI sends JSON to `POST /api/notifications`.
- `NotificationService` validates, writes:
  - `notifications`
  - `notification_dates` (if schedule_type = `dates`), or
  - `notification_recurrence` (if schedule_type = `recurrence`)
  - `notification_subsequents` (if follow-ups exist)

### 2) Scheduler creates jobs
- Every cron tick, `SchedulerService`:
  - Creates `send_jobs` for due dates and due recurrences.
  - For recurring rules, uses `RecurrenceCalculator` and updates `next_trigger_at`.

### 3) Scheduler sends
- For each due job:
  - Creates a placeholder row in `notification_history` (includes `confirm_token`).
  - Sends the message to Telegram (with image if present).
  - Updates history with Telegram message id.
  - Marks job as sent.
  - Creates follow-up jobs for the main send.

### 4) Follow-ups
- Follow-up jobs are created as `send_jobs` with `job_type = subsequent`.
- They use the follow-up message and optional follow-up image (no fallback to the main image).

### 5) Confirmation link
- Each main message includes a button labeled `Complete task`.
- The button points to `/confirm/{token}/`.
- `confirm.php`:
  - Marks the history row as confirmed.
  - Cancels pending follow-ups for that occurrence only.

---

## Scheduling rules (important details)

### Timezones
- All database timestamps are stored in UTC.
- The UI displays times in `display_timezone` (default: `America/Mexico_City`).
- Recurrence calculations are done in the notification timezone, then converted to UTC for storage.

### Schedule types
- `dates`: one-time specific dates stored in `notification_dates`.
  - Optional `yearly_repeat` (if enabled): after a date is sent, the next year's date is auto-inserted.
- `recurrence`: repeating schedules stored in `notification_recurrence` with `next_trigger_at`.

### Recurrence types supported
- `daily`: every N days from `anchor_date`.
- `weekly`: specific weekdays.
- `monthly_date`: a specific day of month.
- `monthly_weekday`: Nth weekday of month (e.g., 2nd Tuesday or last Friday).
- `specific_months`: specific months + day of month.

Weekend adjustment:
- `monthly_date` supports `weekend_adjust = previous_weekday` (move Saturday/Sunday to the previous weekday).

---

## Follow-ups (subsequents)

- Stored in `notification_subsequents` with offsets in minutes/hours/days.
- Each follow-up can have its own `image_path`.
- Jobs link to the original send via `source_history_id`.
- When a confirmation link is used, all pending follow-ups for that history record are cancelled.

---

## Telegram formatting rules

- Messages are sent using Telegram MarkdownV2.
- `TelegramService::formatMarkdownV2()` preserves user formatting for:
  - `*bold*`
  - `_italic_`
  - `~strikethrough~`
  - inline code and code blocks
- All other special characters are escaped to avoid Telegram formatting errors.

---

## Image uploads (where images live)

- Route: `POST /api/upload` with form field `image`.
- Stored under `uploads/YYYY/MM/{random}.{ext}`.
- Database stores the relative path (e.g., `2025/01/abcd1234.jpg`).
- The scheduler resolves the absolute path using `upload_path` from config.

---

## Authentication

- UI uses session auth (login page uses `auth.password`).
- API uses `X-API-Token` header (must match `auth.api_token`).
- Both are handled by `src/auth.php`.
- Session lifetime defaults to 90 days. Override with `session_lifetime` in `config.php`.

---

## API endpoints (current)

Base: `/notifier/api`

Categories:
- `GET /categories`
- `GET /categories/{id}`
- `POST /categories`
- `PUT /categories/{id}`
- `DELETE /categories/{id}`

Notifications:
- `GET /notifications?category_id=&status=&search=`
- `GET /notifications/{id}`
- `POST /notifications`
- `PUT /notifications/{id}`
- `DELETE /notifications/{id}`
- `POST /notifications/{id}/pause`
- `POST /notifications/{id}/resume`
- `GET /notifications/{id}/preview`
- `POST /notifications/{id}/test` (sends to test chat)

Scheduling preview (no save):
- `POST /preview`

Uploads:
- `POST /upload`

History:
- `GET /history?page=&limit=`
- `GET /history/{notificationId}` (also supports `?notification_id=`)

Jobs (admin/debug):
- `GET /jobs`
- `GET /jobs?status=pending,retrying,failed`
- `POST /jobs/{id}/retry`

Test message (raw):
- `POST /test-message`

---

## Database schema (what each table means)

- `categories`
  - Name, color, icon for UI grouping.

- `notifications`
  - Core notification record.
  - `schedule_type`: `dates` or `recurrence`.
  - `timezone`: per-notification timezone (defaults to display timezone).
  - `image_path`: main image for the main notification.
  - `yearly_repeat`: only relevant for `dates`.

- `notification_dates`
  - One row per specific date (UTC).

- `notification_recurrence`
  - Recurrence rules + `next_trigger_at` (UTC).

- `notification_subsequents`
  - Follow-up definitions, offsets, message, optional image.

- `send_jobs`
  - Queue of jobs to send.
  - `job_type`: `main_date`, `main_recurrence`, `subsequent`.
  - Statuses: `pending`, `sending`, `sent`, `retrying`, `cancelled`, `failed`.
  - Backoff on failure (1, 5, 15, 60, 240 minutes; then 8 hours).

- `notification_history`
  - One row per send.
  - Stores `confirm_token` and `confirmed_at`.
  - Used to cancel follow-ups and show history.

- `scheduler_state`
  - Single-row heartbeat table used by cron health checks.
  - Tracks last run, last success, and last error.

- `schema_migrations`
  - Tracks which SQL migrations were applied.

---

## Scheduler behavior (what cron actually does)

- Uses a lock file (configurable) to prevent overlapping runs.
- Resets stuck jobs (status `sending` for > 10 minutes).
- Creates jobs for due dates and due recurrences.
- Sends due jobs in batches with rate limiting.
- Updates recurrence `next_trigger_at` after sending.
- Supports `--status` and `--dry-run` for visibility.

---

## UI pages (what each page is for)

- `dashboard.php` - main overview cards and actions.
- `notifications.php` - list all notifications with filters.
- `notification-edit.php` - create/edit a notification (including follow-ups + images).
- `categories.php` - manage categories and view notifications by category.
- `history.php` - delivery history list.
- `confirm.php` - public confirmation page (no auth).
- `login.php` / `logout.php` - session auth.
- `console.php` - internal docs page (static content; optional).

---

## File and directory guide (top-level)

This is the canonical map of the codebase:

- `api/`
  - `index.php` - single API router + handlers for all endpoints.

- `bootstrap.php`
  - Loads config, enforces UI auth, exposes JS config.

- `categories.php`
  - Categories UI page.

- `config.example.php`
  - Configuration template. Copy to `config.php`.

- `config.php`
  - Active configuration (contains secrets; keep this repo private).

- `confirm.php`
  - Confirmation page for `/confirm/{token}/`.

- `console.php`
  - Optional UI page with a static API reference.

- `dashboard.php`
  - Main dashboard UI.

- `database.sqlite`
  - SQLite database file (runtime data; tracked here for backup).

- `.htaccess`
  - Apache routing rules for clean URLs and the front controller.

- `history.php`
  - History UI page.

- `index.php`
  - Front controller for clean URL routing.

- `js/`
  - `api.js` - API client wrapper used by UI pages.
  - `app.js` - shared UI helpers (dates, navigation, recurrence text).
  - `toast.js` - toast notifications.

- `login.php`, `logout.php`
  - Session login and logout.

- `logs/`
  - Log output destination for cron or server logs.

- `migrations/`
  - `001_initial.sql` - base schema.
  - `002_integration.sql` - UI integration changes.
  - `003_subsequent_images.sql` - follow-up image support.
  - `004_weekend_adjust.sql` - weekend adjust for monthly_date.
  - `005_confirm_links.sql` - confirmation tokens and timestamps.
  - `006_scheduler_state.sql` - scheduler heartbeat table for cron health checks.

- `notification-edit.php`
  - Create/edit UI (includes follow-ups, image upload, preview).

- `notifications.php`
  - Notifications list UI.

- `notifications-setup/`
  - Notification creation helpers (context files, agent guide, generated previews).
  - Not used at runtime by the app.

- `partials/`
  - Shared UI markup (`head.php`, `sidebar.php`, `mobile-nav.php`, `scripts.php`).

- `scheduler.php`
  - Cron entry point for sending jobs.

- `scripts/`
  - `migrate.php` - apply SQL migrations to the database.
  - `cron-health-check.php` - database health checker (sends email when stale/error).
  - `cron-health-check.sh` - shell wrapper for cron.

- `src/`
  - `auth.php` - session + API token auth.
  - `database.php` - PDO wrapper with required PRAGMAs.
  - `services/`
    - `category-service.php` - category CRUD.
    - `job-service.php` - job queue logic and retries.
    - `notification-service.php` - notification CRUD + validation.
    - `recurrence-calculator.php` - recurrence computations.
    - `scheduler-service.php` - cron orchestration.
    - `telegram-service.php` - Telegram API wrapper.

- `uploads/`
  - Storage for uploaded images (runtime data; tracked here for backup).

---

## Setup and deployment (step by step)

1) Configure
- If starting fresh, copy `config.example.php` to `config.php`.
- Fill in Telegram tokens, chat IDs, auth password, `app_url`, and optional `session_lifetime`.
- `config.php` contains secrets; keep this repo private if you commit it.

2) Initialize database
- Run:
  - `php scripts/migrate.php`
- This creates `database.sqlite` and applies all migrations.

3) Web server routing
- If using Apache, keep `.htaccess` in place.
- If using Nginx, route all non-file requests to `index.php` and map `/api` to `api/index.php`.

4) File permissions
- Ensure `uploads/` is writable by your web server.
- If you log cron output, ensure `logs/` is writable.

5) Cron (required)
- Run the scheduler every minute:
  - `* * * * * php /path/to/notifier/scheduler.php >> /path/to/notifier/logs/scheduler.log 2>&1`
- Verify the cron entries are installed for the correct system user:
  - `crontab -l`

5b) Cron health check (recommended)
- Run the health checker every 10 minutes:
  - `*/10 * * * * /path/to/notifier/scripts/cron-health-check.sh >> /path/to/notifier/logs/cron-health.log 2>&1`
- Configure email settings in `config.php` under `cron_health`.
- The health check uses the database heartbeat in `scheduler_state` and sends email on stale runs or errors.
- Email can be sent via PHP `mail()` (requires a local MTA like Postfix) or MailerSend.
- For MailerSend, set `cron_health.method` to `mailersend` and configure `mailer_send` with API key and sender.

6) Login
- Navigate to `/notifier/login/` and authenticate using `auth.password`.

---

## Maintenance notes

- If messages stop sending, check the cron log and `send_jobs` status.
- Failed jobs automatically retry with backoff; they eventually become `failed` after many attempts.
- Use `GET /api/jobs?status=failed` and `POST /api/jobs/{id}/retry` for manual recovery.
- Confirmation links are per-occurrence; confirming one instance does not affect future occurrences.

---
