---
module: fleet.patch-windows
title: Patch Window Guide
tab: Claude
order: 40
audience: dev
---

Act-without-guessing cheat-sheet. Everything confirmed in `hiveops-fleet` source.

## Ownership
- **Service:** `hiveops-fleet` ONLY (`com.hiveops.fleet`, port 8097, DB `hiveiq_fleet`).
- Patch-window assignment authority = fleet's `patch_window_devices` table. **NOT** devices, **NOT** mgmt, **NOT** `device_summary` (its `patch_window_id` column was dropped in V4).

## Tables (DB `hiveiq_fleet`)
| Table | Key columns |
|-------|-------------|
| `patch_windows` | `id, institution_key, name, days_of_week (comma string), week_of_month (nullable), start_time, end_time, timezone` · UNIQUE `(institution_key, name)` |
| `patch_window_devices` | `id, patch_window_id (FK→patch_windows, ON DELETE CASCADE), atm_id, institution_key` · UNIQUE `(patch_window_id, atm_id)` |

## Endpoints (base `/api/fleet/patch-windows`)
| Method + Path | Roles |
|---------------|-------|
| GET `/api/fleet/patch-windows` (`?institutionKey=`) | USER/CUSTOMER/ADMIN/MSP_ADMIN/BCOS_ADMIN |
| GET `/api/fleet/patch-windows/{id}` | read roles |
| POST `/api/fleet/patch-windows` | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| PUT `/api/fleet/patch-windows/{id}` | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| DELETE `/api/fleet/patch-windows/{id}` | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| GET `/api/fleet/patch-windows/{id}/atms` | read roles |
| PUT `/api/fleet/patch-windows/{windowId}/atms/{atmId}` | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| DELETE `/api/fleet/patch-windows/{windowId}/atms/{atmId}` | ADMIN/MSP_ADMIN/BCOS_ADMIN |

- Create body (`CreatePatchWindowRequest`): `institutionKey, name, daysOfWeek[], weekOfMonth?, startTime, endTime, timezone`.
- Update reuses the same body; `institutionKey` is ignored on update (window's own key kept).

## Kafka
- Patch-window CRUD emits/consumes **nothing**.
- Fleet consumes `hiveops.device.events` (`DeviceEventConsumer`, group `hiveops-fleet-device-sync`) → `device_summary`.
- Fleet produces `hiveops.fleet.task.events` (`FleetTaskEventPublisher`) for the scheduled task lifecycle.

## Scheduling contract
- Only `HOTFIX_INSTALL` + `SOFTWARE_INSTALL` are window-scheduled, and only when the create request has **no** explicit `scheduledAt`.
- These are created as status `PENDING_APPROVAL` (need approval before they can dispatch).
- Immediate `HOTFIX` / `SOFTWARE` (download) tasks are **NOT** window-gated.
- `scheduled_at` is set to the window's next **start** slot, stored in **UTC**.
- Dispatch gate (`GET /api/internal/fleet/tasks/next`, `ROLE_INTERNAL`/`X-Internal-Secret`): returns a `PENDING` task only if `scheduledAt == null || scheduledAt <= now`. Start-gated only — **no end-time cutoff**.

## Gotchas (confirmed in code)
- `days_of_week` is stored raw. `PatchWindowService.nextOccurrence` parses it with `DayOfWeek.valueOf(...)` → needs FULL names (`MONDAY`). The UI stores short codes (`MON`) → parse throws → caught → `scheduledAt` stays null → install runs immediately after approval. If "window ignored", check this first.
- List filter: controller param is `institutionKey`; UI sends `institution` → UI filter is silently ignored (scope still applies).
- DTO returns `deviceCount`; frontend reads `atmCount` (drift; DB count is authoritative via `countByPatchWindowId`).
- One-window-per-ATM is NOT DB-enforced; scheduler uses `findByAtmId(...).findFirst()`.
- `create()` does NOT scope-check (trusts body `institutionKey`); every other op runs `assertInScope` (out-of-scope → 403). Scope is non-null for CUSTOMER + MSP_ADMIN; null (unrestricted) for BCOS_ADMIN/ADMIN/USER.
- Name unique per institution → duplicate name = 409.

## Do NOT
- Do NOT write patch-window / assignment data into `hiveops-devices`, `hiveops-mgmt`, or `device_summary.patch_window_id` (removed). Fleet + `patch_window_devices` only.
- Do NOT assume a window blocks installs after its end time — it does not.
- Do NOT expect `HOTFIX`/`SOFTWARE` (non-`_INSTALL`) tasks to be scheduled.
- Do NOT expect a UI-created window to actually schedule until `days_of_week` normalization is fixed.
- Do NOT rely on the `?institution=` query param for filtering (use `institutionKey`).
- Do NOT set `scheduled_at` in the past to force dispatch without approval — status is still `PENDING_APPROVAL`.
