Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.5.0] - Unreleased

### Added

- Pagination cap warning -- the poller now logs a warning when the page limit is reached and more results are available, suggesting the user narrow their repo filter
- Configurable indicator settings via a new `[indicator]` TOML section -- `reconnect_interval`, `window_width`, and `max_window_height` can now be tuned without code changes
- Unknown config key warnings -- `load_config()` now logs a warning for any unrecognised top-level key, helping catch typos early
- Repo-based notification grouping via a new `[notifications]` config section -- set `grouping = "repo"` to receive per-repository summary notifications instead of a single flat list
- Per-repo notification overrides via `[notifications.repos."owner/repo"]` -- disable notifications for noisy repos (`enabled = false`), override urgency (`urgency = "critical"`), or set a custom individual-vs-summary threshold per repository

### Changed

- Improved first-run experience -- `ConfigError` is now caught and displayed as a user-friendly log message instead of a raw traceback. Missing config suggests running `forgewatch setup`, invalid config suggests checking the config file. The daemon exits cleanly with code 1
- Logging is now initialised before config loading so that config errors are properly formatted
- Config validation now collects all errors and reports them in a single `ConfigError`, so users can fix every problem in one pass instead of playing whack-a-mole
- Validation error messages now include actionable hints (e.g. `ghp_` token prefix example, `GitHub recommends 300s` for poll interval, `octocat/Hello-World` for repo format)

## [1.4.1] - 2026-03-12

### Fixed
Expand Down
17 changes: 17 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ repos = []
# Use "light" for light desktop panels (dark icons on light background).
# Use "dark" for dark desktop panels (light icons on dark background).
# icon_theme = "light"

# [notifications]
# grouping = "flat" # "flat" (default) or "repo"
#
# [notifications.repos."owner/repo"]
# enabled = true # Set to false to suppress notifications
# urgency = "normal" # "low", "normal", or "critical"
# threshold = 3 # Individual vs summary threshold

# ---------------------------------------------------------------------------
# Indicator settings (system tray process)
# ---------------------------------------------------------------------------

# [indicator]
# reconnect_interval = 10 # Seconds between reconnect attempts (default: 10)
# window_width = 400 # Popup window width in pixels (default: 400)
# max_window_height = 500 # Maximum popup window height in pixels (default: 500)
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ The system is designed to be resilient to transient failures:
| HTTP 401 | Raise `AuthError` immediately (bad token, no point retrying) |
| HTTP 403 | Respect `Retry-After` header, retry once, then return empty |
| Rate limit exhausted | Preemptively wait until reset time before making request |
| Invalid config | Raise `ConfigError` at startup (fail fast) |
| Invalid config | Catch `ConfigError`, log user-friendly message with remediation hint (`forgewatch setup` for missing config, "check your config file" for validation errors), exit with code 1 |

The daemon should never crash from a transient GitHub API issue. It logs the
error and continues with the next poll cycle.
Expand Down
103 changes: 100 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,37 @@ If no config file is found at the resolved path, a `ConfigError` is raised.
| `notification_urgency` | string | No | `"normal"` | Notification urgency: `"low"`, `"normal"`, or `"critical"` |
| `icon_theme` | string | No | `"light"` | Icon theme for the system tray indicator: `"light"` (dark icons for light panels) or `"dark"` (light icons for dark panels) |

### `[notifications]` section

Settings for notification grouping and per-repo overrides. These affect how
desktop notifications are grouped and allow fine-grained control per repository.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `grouping` | string | No | `"flat"` | Grouping mode: `"flat"` (single list) or `"repo"` (grouped by repository) |

#### `[notifications.repos."owner/repo"]` sub-tables

Per-repo notification overrides. Each key is a repository in `owner/name`
format. Repos without an entry use the global defaults.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `enabled` | boolean | No | `true` | Set to `false` to suppress notifications for this repo |
| `urgency` | string | No | `"normal"` | Notification urgency: `"low"`, `"normal"`, or `"critical"` |
| `threshold` | integer | No | `3` | Individual vs. summary threshold for this repo (minimum: 1) |

### `[indicator]` section

Settings for the system tray indicator process. These are read by the
indicator via `load_indicator_config()` and have no effect on the daemon.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `reconnect_interval` | integer | No | `10` | Seconds between D-Bus reconnect attempts (minimum: 1) |
| `window_width` | integer | No | `400` | Popup window width in pixels (minimum: 200) |
| `max_window_height` | integer | No | `500` | Maximum popup window height in pixels (minimum: 200) |

## GitHub token setup

ForgeWatch needs a GitHub personal access token (PAT) to query the
Expand Down Expand Up @@ -128,8 +159,12 @@ config file without a token and supply it via the environment instead.

## Validation rules

All validation happens at config load time. If any rule fails, a `ConfigError`
is raised with a descriptive message.
All validation happens at config load time. Validation collects **all** errors
and raises a single `ConfigError` listing every problem, so you can fix
everything in one pass. Error messages include actionable hints where possible
(e.g. example token prefix, recommended poll interval).

Unrecognised top-level keys produce a log warning (possible typo detection).

- `github_token` -- must be a non-empty string
- `github_username` -- must be a non-empty string
Expand All @@ -146,6 +181,22 @@ is raised with a descriptive message.
- `notification_urgency` -- must be one of `low`, `normal`, `critical` (case-insensitive)
- `icon_theme` -- must be one of `light`, `dark` (case-insensitive)

**`[notifications]` section:**

- `notifications` -- must be a table (if present)
- `notifications.grouping` -- must be one of `flat`, `repo` (case-insensitive)
- `notifications.repos` -- must be a table (if present)
- Each `notifications.repos."owner/repo"` entry must be a table with:
- `enabled` -- must be a boolean
- `urgency` -- must be one of `low`, `normal`, `critical` (case-insensitive)
- `threshold` -- must be an integer >= 1

**`[indicator]` section:**

- `reconnect_interval` -- must be an integer >= 1
- `window_width` -- must be an integer >= 200
- `max_window_height` -- must be an integer >= 200

## Example config

A minimal configuration with only required fields:
Expand Down Expand Up @@ -174,6 +225,21 @@ max_retries = 5
notification_threshold = 5
notification_urgency = "low"
icon_theme = "light"

[notifications]
grouping = "repo"

[notifications.repos."myorg/frontend"]
urgency = "critical"
threshold = 5

[notifications.repos."myorg/noisy-repo"]
enabled = false

[indicator]
reconnect_interval = 10
window_width = 400
max_window_height = 500
```

Using environment variables instead of a token in the file:
Expand Down Expand Up @@ -237,6 +303,27 @@ repos = []
# Use "light" for light desktop panels (dark icons on light background).
# Use "dark" for dark desktop panels (light icons on dark background).
# icon_theme = "light"

# ---------------------------------------------------------------------------
# Notification grouping and per-repo overrides
# ---------------------------------------------------------------------------

# [notifications]
# grouping = "flat" # "flat" (default) or "repo"
#
# [notifications.repos."owner/repo"]
# enabled = true # Set to false to suppress notifications
# urgency = "normal" # "low", "normal", or "critical"
# threshold = 3 # Individual vs summary threshold

# ---------------------------------------------------------------------------
# Indicator settings (system tray process)
# ---------------------------------------------------------------------------

# [indicator]
# reconnect_interval = 10 # Seconds between reconnect attempts (default: 10)
# window_width = 400 # Popup window width in pixels (default: 400)
# max_window_height = 500 # Maximum popup window height in pixels (default: 500)
```

## Runtime changes via SIGHUP
Expand All @@ -258,7 +345,7 @@ immediately.

```python
from pathlib import Path
from forgewatch.config import load_config
from forgewatch.config import load_config, load_indicator_config

# Load from default path
cfg = load_config()
Expand All @@ -282,4 +369,14 @@ print(cfg.max_retries) # 3
print(cfg.notification_threshold) # 3
print(cfg.notification_urgency) # "normal"
print(cfg.icon_theme) # "light"

# Access notification grouping settings
print(cfg.notifications.grouping) # "flat"
print(cfg.notifications.repos) # {} (or dict of RepoNotificationConfig)

# Load indicator-specific config ([indicator] section)
ind = load_indicator_config()
print(ind.reconnect_interval) # 10
print(ind.window_width) # 400
print(ind.max_window_height) # 500
```
Loading
Loading