trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
# 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:

| Field | CustomCommands         | Jira                 |
|-------|------------------------|----------------------|
| 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.