# Integrations Module Architecture
## Overview
The integrations module provides a pluggable system for work logging and issue opening. Users switch between integration backends in Settings (field 0). The active backend is persisted in `config.integration`.
## Module Structure
```
integrations/
mod.rs - IntegrationKind enum, IntegrationOutput, dispatch functions
secrets.rs - SecretsManager (keyring + file fallback)
jira.rs - Jira Cloud REST API implementation
```
## IntegrationKind
Serializable enum stored in `~/.timetrack.toml`:
- **CustomCommands** (default) - User-defined shell commands with `[[variable]]` substitution. The dispatch functions return `None`, signaling the caller in `app.rs` to fall through to the existing shell execution logic.
- **Jira** - Built-in Jira Cloud integration. Dispatch functions return `Some(IntegrationOutput)`.
## Dispatch Pattern
`log_work()` and `open_issue()` in `mod.rs` accept the integration kind and return `Option<IntegrationOutput>`:
- `None` = CustomCommands, caller falls through to shell logic in `app.rs`
- `Some(output)` = built-in integration handled it; caller logs messages and optionally auto-toggles logged status
This keeps `app.rs` from needing to know integration internals - it just checks the Option.
## SecretsManager
Stores sensitive values (API tokens) outside the config file.
**Backend selection** (at startup):
1. Probe the system keyring by writing/deleting a test entry
2. If keyring works, use it (service name: `"trackwork"`)
3. If keyring fails, fall back to `~/.timetrack.secrets` (TOML file, chmod 600 on Unix)
**API**: `get(key)`, `set(key, value)`, `delete(key)`
Currently stored keys:
- `jira_api_token` - Jira Cloud API token
## Jira Cloud Integration
### log_work()
- POST to `{jira_url}/rest/api/2/issue/{issue_key}/worklog`
- Basic auth: base64(`email:api_token`)
- Body: `{ "timeSpent": "Xm", "started": "ISO timestamp", "comment": "description" }`
- Uses `reqwest::blocking::Client`
### fetch_issue_summary()
- GET `{jira_url}/rest/api/2/issue/{issue_key}?fields=summary`
- Basic auth: base64(`email:api_token`)
- Returns the issue summary string (task name)
- Dispatched via `mod.rs:fetch_issue_summary()` which returns `Option<Result<String, String>>`
- Used by `App::sync_task_name_from_jira()` to populate the `tasks` table `name` field
- Auto-called on entry create/edit when issue_key is set and task name is not yet cached
- Bulk-called via Operations Menu → "Sync all task names from Jira"
### open_issue()
- Opens `{jira_url}/browse/{issue_key}` via platform browser command (`xdg-open` / `open` / `start`)
## Adding a New Integration
1. Add variant to `IntegrationKind` enum (derives Serialize/Deserialize)
2. Update `display_name()`, `cycle_next()`, `Default`
3. Create `src/integrations/new_backend.rs` with `log_work()` and `open_issue()`
4. Add dispatch arm in `mod.rs:log_work()` and `mod.rs:open_issue()`
5. Add any new config fields to `config.rs`
6. Update settings field layout in `settings/ui.rs` and `settings/input.rs`
7. Update field count in `app.rs:next_field()` and `previous_field()`
## Settings Field Layout
Field indices shift based on active integration:
| 0 | Integration selector | Integration selector |
| 1 | Log Work Command (l) | Jira URL |
| 2 | Open Issue Command (o) | Jira Email |
| 3 | Date Format | API Token (masked) |
| 4 | Legacy Time Format | Date Format |
| 5 | Change Passphrase btn | Legacy Time Format |
| 6 | Triggers btn | Change Passphrase btn|
| 7 | Colors 1 (start) | Triggers btn |
| 7-12 | Colors 1-6 | - |
| 8-13 | - | Colors 1-6 |
Total fields: CustomCommands = 13, Jira = 14
The Passphrase and Triggers buttons are action fields (Enter/Space opens a sub-screen — `PassphraseChange` / `Triggers` InputMode), not editable text. When adding/removing a settings field, update the counts in `app.rs:next_field()`/`previous_field()`, the `colors_start` in `cycle_color()` and `settings/input.rs:is_color_field()`, the index helpers `settings_*_field()`, and the `update_field_label()` + `settings/ui.rs` match arms.
## Event Triggers (webhooks)
`TriggersConfig` in `config.rs` holds three `TriggerConfig { enabled, url, body }` entries: `day_start`, `ooo_start`, `ooo_end`. The `triggers/` module owns the `Triggers` InputMode sub-screen (`ui.rs`/`input.rs`), the `TriggerEvent` enum + `EVENT_LABELS`, and `send(url, body)` (blocking reqwest POST, `Content-Type: application/json`).
Firing happens in `app.rs::fire_trigger(event, description)` which substitutes `[[event]] [[date]] [[time]] [[datetime]] [[description]]` into the body template. Hook points:
- **DayStart** — `maybe_fire_day_start()` (guarded once/day via `app_settings` key `trigger_day_start_fired`), called after entry create (`save_entry`) and manual clock-in (`toggle_at_work_start`).
- **OooStart** — `save_entry()` when a new off-work entry is created (`f` / `start_creating_off_work`).
- **OooEnd** — `stop_or_restart_entry()` when a running off-work entry is stopped.