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
52 changes: 40 additions & 12 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,22 @@ Paginated response envelope:

```json
{
"agent_id": 1,
"data": [ ... ],
"count": 42,
"limit": 100,
"offset": 0
}
```

### `agent_id` field (since v4.6.1)

Every response from an agent-specific endpoint includes `"agent_id"` as the **first field**.
This resolves the ambiguity that job IDs are only unique per agent — in multi-agent setups,
the same numeric ID may refer to different jobs on different agents.

Endpoints that are not agent-specific (`GET /api/v1/agents`) do not carry `agent_id`.

---

## 6. Error Responses
Expand Down Expand Up @@ -306,6 +315,7 @@ List all cron jobs.

```json
{
"agent_id": 1,
"data": [
{
"id": 1,
Expand Down Expand Up @@ -348,7 +358,7 @@ List all cron jobs.

Get a single cron job by ID.

**Response 200:** Single job object (same structure as in the list, without envelope).
**Response 200:** Single job object (same structure as in the list, without envelope, but with `agent_id` as the first field).

**Response 404:**

Expand Down Expand Up @@ -414,7 +424,7 @@ Delete a cron job and remove it from the crontab. Scope: **`jobs:write`**
**Response 200:**

```json
{ "success": true }
{ "agent_id": 1, "success": true }
```

---
Expand All @@ -426,7 +436,7 @@ Trigger an immediate one-time execution of the job. Scope: **`jobs:execute`**
**Response 200:**

```json
{ "success": true, "message": "Job queued for immediate execution." }
{ "agent_id": 1, "success": true, "message": "Job queued for immediate execution." }
```

---
Expand All @@ -438,7 +448,7 @@ Kill a running execution by its execution log ID. Scope: **`jobs:execute`**
**Response 200:**

```json
{ "success": true }
{ "agent_id": 1, "success": true }
```

---
Expand Down Expand Up @@ -500,6 +510,7 @@ List all tags.

```json
{
"agent_id": 1,
"data": [
{ "id": 1, "name": "backup" },
{ "id": 2, "name": "monitoring" }
Expand Down Expand Up @@ -576,6 +587,7 @@ List all maintenance windows.

```json
{
"agent_id": 1,
"data": [
{
"id": 1,
Expand All @@ -597,7 +609,7 @@ List all maintenance windows.

Get a single maintenance window.

**Response 200:** Single window object.
**Response 200:** Single window object (with `agent_id` as the first field).

---

Expand Down Expand Up @@ -638,7 +650,7 @@ Delete a maintenance window. Scope: **`maintenance:write`**
**Response 200:**

```json
{ "success": true }
{ "agent_id": 1, "success": true }
```

---
Expand All @@ -648,7 +660,10 @@ Delete a maintenance window. Scope: **`maintenance:write`**
Required scope for read: **`settings:read`**
Required scope for write: **`settings:write`**

Settings are grouped into sections: `mail`, `telegram`, `influxdb`, `notifications`.
Settings are grouped into sections: `mail`, `telegram`, `influxdb`, `notifications`, `performance_monitor`, `web`.

The `web` section is **read-only via the API** — it is populated automatically when the web container
pushes its identity to the agent (startup, agent create/update/select).

---

Expand Down Expand Up @@ -681,7 +696,8 @@ Read all agent settings.
"org": "",
"bucket": ""
},
"notifications": {
"web": {
"web_agent_id": 3,
"web_url": "https://cronmanager.example.com"
}
}
Expand All @@ -691,7 +707,7 @@ Read all agent settings.

### GET /api/v1/settings/{section}

Read a single settings section (`mail`, `telegram`, `influxdb`, `notifications`).
Read a single settings section (`mail`, `telegram`, `influxdb`, `notifications`, `web`).

**Response 200:** Section object.

Expand All @@ -701,6 +717,9 @@ Read a single settings section (`mail`, `telegram`, `influxdb`, `notifications`)

Update a settings section. Scope: **`settings:write`**

Writable sections: `mail`, `telegram`, `influxdb`, `notifications`, `performance_monitor`.
The `web` section is silently skipped — use the agent identity push instead.

**Request body:** Partial or full section object (only provided keys are updated).

**Response 200:** Updated section object.
Expand All @@ -714,7 +733,7 @@ Resync crontab from database. Scope: **`settings:write`**
**Response 200:**

```json
{ "success": true, "message": "Crontab resynced." }
{ "agent_id": 1, "success": true, "message": "Crontab resynced." }
```

---
Expand Down Expand Up @@ -742,6 +761,7 @@ Execution history across all jobs.

```json
{
"agent_id": 1,
"data": [
{
"execution_id": 101,
Expand Down Expand Up @@ -791,19 +811,24 @@ List all agents visible to this API key.
"id": 1,
"name": "Default",
"description": "Lokaler Agent",
"enabled": true
"enabled": true,
"web_url": "https://cronmanager.example.com"
},
{
"id": 2,
"name": "Remote-Server",
"description": "Agent auf server2.example.com",
"enabled": true
"enabled": true,
"web_url": "https://cronmanager.example.com"
}
],
"count": 2
}
```

`web_url` is the public base URL of the web container (from `app.web_url` in the web config).
It is the same value for all agents — `null` when not configured.

**Response 401 / 403:** See §6 Error Responses.

---
Expand Down Expand Up @@ -837,6 +862,7 @@ Return a paginated list of audit log entries with optional filters.

```json
{
"agent_id": 1,
"data": [
{
"id": 42,
Expand Down Expand Up @@ -956,6 +982,8 @@ to 60 seconds of delay before the daemon picks up the entry.

| Version | Change |
|---|---|
| 4.6.1 | Every agent-specific endpoint now includes `"agent_id"` as the first field in its response (jobs, maintenance, export/json, audit, settings, timeline, tags). Resolves ambiguity in multi-agent setups where the same numeric job ID may refer to different jobs on different agents. UI links (notifications, breadcrumbs, filter resets, pagination) now carry `?agent_id=X` throughout. |
| 4.6.0 | Added `web` section to `GET /api/v1/settings` (read-only, push-managed; contains `web_agent_id` and `web_url`); added `web_url` field to `GET /api/v1/agents` response; `PUT /api/v1/settings` silently ignores the `web` section |
| 4.5.0 | Added `notify_on_silence` (bool), `silence_grace_minutes` (int\|null), `last_silence_alert_at` (string\|null, read-only) to job objects; `GET /health` extended with `silent_jobs` (int\|null) and `last_execution_at` (string\|null) |
| 4.3.4 | Added `GET /api/v1/audit` endpoint (`audit:read` scope, admin-only); added §15 Audit Log |
| 4.2.0 | Added `GET /api/v1/agents` endpoint (`settings:read` scope; respects `agent_ids` restriction; omits sensitive fields) |
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [4.6.1] – branch: `feature/agent-web-identity`

### Added

- **`agent_id` in allen API-Antworten**: Jeder agent-spezifische Endpunkt enthält `"agent_id"` als
erstes Feld in seiner Antwort (Jobs, Maintenance Windows, Export/JSON, Audit, Settings, Timeline,
Tags). Im Multi-Agent-Betrieb sind Job-IDs nur pro Agent eindeutig; das neue Feld verhindert
Ambiguitäten beim programmatischen Auswerten von Responses.

### Fixed

- **UI-Links tragen `?agent_id=X`**: Alle internen Links (Sidebar-Navigation, Breadcrumbs,
Paginierung, Filter-Reset, Job-Detail, Monitor, Edit, Kopieren, Import, Export, Audit, Timeline,
Maintenance-Windows, Dashboard-Filter) enthalten jetzt `?agent_id=X`, wenn ein Agent aktiv ist.
Zuvor fehlte der Parameter, sodass geteilte Links oder E-Mail-/Telegram-Benachrichtigungen
nach dem Login beim falschen Agent landeten.
- **Weiterleitungen nach POST-Aktionen**: Alle Controller-Redirects nach Formulareingaben
(`store`, `update`, `destroy`, `bulkAction`, `executeNow`, `importStore`) nutzen
`agentPath()` und erhalten `?agent_id=X`.

---

## [4.6.0] – branch: `feature/agent-web-identity`

### Added

- **Agent-aware Notification Links**: E-Mail- und Telegram-Benachrichtigungen enthalten jetzt
`?agent_id=X` im Link, wenn die Web-Identity konfiguriert ist. Klicks landen direkt beim richtigen
Agent – auch nach Session-Ablauf (Login-Redirect bewahrt den Parameter).
- **Web-Identity-Push**: Der Web-Container teilt jedem Agent beim Start sowie beim Anlegen,
Bearbeiten und Auswählen eines Agents seine öffentliche URL und die Agent-ID mit
(`PUT /settings/web-identity`, HMAC-gesichert). Agents speichern beides in `agent_settings`
(Section `web`).
- **`AgentIdentityPusher`** (Web): neuer Service `web/src/Service/AgentIdentityPusher.php`
– kapselt Push-Logik, schluckt Verbindungsfehler und loggt nur eine Warnung.
- **`WEB_URL` im Web-Container**: die Env-Variable wandert vom Agent- in den Web-Container
(`WEB_URL` → `app.web_url` in `config.json`). Agents beziehen die URL künftig ausschließlich
per Push, nicht mehr aus ihrem eigenen `config.json`.
- **`BaseController::selectedAgent()`** prüft jetzt zuerst `$_GET['agent_id']`: Notification-Links
aktivieren direkt den richtigen Agent und schreiben ihn in die Session.
- **REST API – `GET /api/v1/agents`**: neues Feld `web_url` (string|null) in jedem Agent-Objekt.
- **REST API – `GET /api/v1/settings/web`**: gibt die gespeicherte Web-Identity zurück
(`{"web_agent_id": N, "web_url": "..."}`, Scope `settings:read`).
`PUT /api/v1/settings/web` gibt 405 zurück – die Section wird ausschließlich per Push verwaltet.
- **Neue Tests**: `WebIdentityEndpointTest` (Integration), `NotificationUrlBuilderTest` (Unit,
prüft `buildNotificationUrl()` in MailNotifier und TelegramNotifier via Reflection),
drei neue Fälle in `AgentsApiControllerTest` für das `web_url`-Feld.

### Changed

- `MailNotifier` und `TelegramNotifier` lesen `web_url` und `web_agent_id` jetzt aus der
DB-gesicherten Section `web` (statt `notifications.web_url`).
- `send-notification.php` nutzt `DbConfig` (statt rohem Noodlehaus-Config) damit die
DB-gesicherten Werte in Hintergrundprozessen verfügbar sind.

---

## [4.5.0] – branch: `feature/silence-detection`

### Added
Expand Down
2 changes: 1 addition & 1 deletion agent/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.5.0
4.6.1
5 changes: 5 additions & 0 deletions agent/agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ function jsonResponse(int $statusCode, array $data): void

// -- Persistent agent settings (notification / integration config in DB) --

// PUT /settings/web-identity must be registered before the generic /settings
// routes so the router does not mis-match the static path segment.
$webIdentity = new \Cronmanager\Agent\Endpoints\Settings\WebIdentityEndpoint($dbConfig, $logger);
$router->addRoute('PUT', '/settings/web-identity', [$webIdentity, 'handle']);

$settingsEndpoint = new \Cronmanager\Agent\Endpoints\SettingsEndpoint($dbConfig, $logger, $auditLogger);
$router->addRoute('GET', '/settings', [$settingsEndpoint, 'handle']);
$router->addRoute('PUT', '/settings', [$settingsEndpoint, 'handle']);
Expand Down
9 changes: 7 additions & 2 deletions agent/bin/send-notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
});

use Cronmanager\Agent\Bootstrap;
use Cronmanager\Agent\Config\DbConfig;
use Cronmanager\Agent\Database\Connection;
use Cronmanager\Agent\Notification\MailNotifier;
use Cronmanager\Agent\Notification\TelegramNotifier;

Expand Down Expand Up @@ -100,6 +102,9 @@
$logger = $bootstrap->getLogger();
$config = $bootstrap->getConfig();

$pdo = Connection::getInstance()->getPdo();
$dbConfig = new DbConfig($config, $pdo);

$jobId = (int) ($data['job_id'] ?? 0);
$description = (string) ($data['description'] ?? '');
$linuxUser = (string) ($data['linux_user'] ?? '');
Expand All @@ -110,8 +115,8 @@
$finishedAt= (string) ($data['finished_at'] ?? '');
$target = (string) ($data['target'] ?? '');

$mailNotifier = new MailNotifier($logger, $config);
$telegramNotifier = new TelegramNotifier($logger, $config);
$mailNotifier = new MailNotifier($logger, $dbConfig);
$telegramNotifier = new TelegramNotifier($logger, $dbConfig);

if ($type === 'silence') {
$lastStartedAt = isset($data['last_started_at']) && $data['last_started_at'] !== null
Expand Down
4 changes: 2 additions & 2 deletions agent/src/Config/DbConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* 1. Key prefix matches a DB-managed section AND a row exists in DB → DB value.
* 2. Otherwise → delegate to the wrapped Noodlehaus\Config (config.json).
*
* DB-managed sections: mail, telegram, influxdb, notifications.
* DB-managed sections: mail, telegram, influxdb, performance_monitor, web.
* Infrastructure keys (agent.*, database.*, logging.*, cron.*) are always
* read from config.json because they are needed before the DB is available.
*
Expand All @@ -42,7 +42,7 @@ final class DbConfig implements ConfigInterface
// -------------------------------------------------------------------------

/** Sections whose values are stored in and read from the database. */
private const DB_SECTIONS = ['mail', 'telegram', 'influxdb', 'notifications', 'performance_monitor'];
private const DB_SECTIONS = ['mail', 'telegram', 'influxdb', 'notifications', 'performance_monitor', 'web'];

/**
* Sensitive fields that are encrypted at rest when AGENT_SETTINGS_KEY is set.
Expand Down
Loading
Loading