---
module: fleet.patch-windows
title: Patch Window Guide
tab: Internal
order: 20
audience: internal
---

Ops / support view for HiveIQ Patch Windows. Owned by **hiveops-fleet** (backend port 8097, DB `hiveiq_fleet`, frontend `fleet.bcos.cloud`). Component: `FleetManagement/PatchWindows.svelte`.

## What it actually does

- A patch window is a recurring maintenance slot (`patch_windows` row) scoped to one **institution**.
- Devices are attached to a window via the join table `patch_window_devices` (one row per `patch_window_id` + `atm_id`).
- On task creation, `HOTFIX_INSTALL` and `SOFTWARE_INSTALL` tasks with **no explicit `scheduledAt`** get `scheduledAt` set to the window's **next start time** (`PatchWindowService.nextOccurrence`). Immediate `HOTFIX` / `SOFTWARE` (download) tasks are **not** window-gated.
- The agent poll (`GET /api/internal/fleet/tasks/next`) only returns a PENDING task when `scheduledAt == null || scheduledAt <= now`. That single filter is the whole enforcement mechanism — dispatch is gated on the window **start**; there is no hard stop at the window **end**.

## Permissions / roles

| Action | Endpoint | `@PreAuthorize` |
|--------|----------|-----------------|
| List / view windows | GET `/api/fleet/patch-windows`, GET `/{id}` | `USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| View assigned devices | GET `/{id}/atms` | `USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| Create window | POST `/api/fleet/patch-windows` | `ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| Update window | PUT `/{id}` | `ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| Delete window | DELETE `/{id}` | `ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| Assign device | PUT `/{windowId}/atms/{atmId}` | `ADMIN, MSP_ADMIN, BCOS_ADMIN` |
| Unassign device | DELETE `/{windowId}/atms/{atmId}` | `ADMIN, MSP_ADMIN, BCOS_ADMIN` |

- **Institution scoping:** every read/mutate except `create` runs `InstitutionContext.resolveScope()` and `assertInScope(...)`. Scope is the JWT credentials list for `CUSTOMER` and `MSP_ADMIN` (they see only their own institutions → out-of-scope IDs return **403**). `BCOS_ADMIN` / `ADMIN` / `USER` resolve to `null` = unrestricted.
- **`create` does not scope-check** — it trusts the `institutionKey` in the request body. A scoped user who can reach POST could create a window for another institution; guard this at the caller/UI, not the service.

## First things a support engineer checks

1. **"My HOTFIX_INSTALL ran immediately / ignored the window."** Check the stored `days_of_week` value. The UI sends short codes (`MON,TUE,…`) but `nextOccurrence` calls `DayOfWeek.valueOf(...)`, which requires full names (`MONDAY,…`). A short-code value throws inside `nextOccurrence`; `FleetTaskService` **catches it, logs a WARN, leaves `scheduledAt = null`**, and the install becomes immediately dispatchable after approval. This is the #1 "window didn't hold" cause — verify the row in `patch_window_devices` exists AND the `patch_windows.days_of_week` value parses as `DayOfWeek`.
2. **Is the device even attached?** `SELECT * FROM patch_window_devices WHERE atm_id = '<AT…>';` No row → task is never window-scheduled (runs after approval).
3. **Approval gate.** `HOTFIX_INSTALL` / `SOFTWARE_INSTALL` are created as `PENDING_APPROVAL`, not `PENDING`. Even a correctly scheduled task will not dispatch until an approver acts (`FLEET_APPROVER` authority via `fleetTaskApprover` JWT claim, or `BCOS_ADMIN`). "Nothing happened at 2am" is often an unapproved task.
4. **Timezone.** `nextOccurrence` computes the next slot in the window's `timezone` then stores `scheduled_at` as **UTC**. If the slot looks off by hours, confirm the window `timezone` (default `UTC`; UI default `America/New_York`).
5. **Task not dispatching after start time.** The agent must be polling and the device row must exist in `device_summary` (synced from Kafka) — `GET /tasks/next` returns 204 for an unknown `atm`.

## Common failure modes → fix

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Install ran outside/immediately, window ignored | `days_of_week` stored as `MON,TUE…` → `nextOccurrence` throws, `scheduledAt` stays null | Log shows `Could not compute next patch window for device …`. Store full `DayOfWeek` names, or treat window scheduling as best-effort until reconciled. |
| Device count column shows blank/`undefined terminals` | DTO returns `deviceCount`; frontend reads `atmCount` | Cosmetic frontend/DTO field mismatch — count is correct in the DB (`SELECT count(*) FROM patch_window_devices WHERE patch_window_id = ?`). |
| Filtering by institution returns everything | UI sends `?institution=`; controller reads `?institutionKey=` | The query param is silently ignored; scoping still applies. Filter client-side or pass `institutionKey`. |
| 403 on view/edit | Caller's JWT scope doesn't include the window's `institution_key` | Expected for `CUSTOMER`/`MSP_ADMIN` acting cross-institution. |
| 409 on create/update | `existsByInstitutionKeyAndName` | Window name must be unique per institution. Rename. |
| Task dispatched but well after window end | Only the **start** is gated; a task eligible at start stays dispatchable indefinitely | By design today — there is no end-time cutoff in the dispatch filter. |
| ATM appears in two windows | Schema only enforces `UNIQUE(patch_window_id, atm_id)`, not one-window-per-ATM | `findByAtmId(...).findFirst()` picks an arbitrary window. Unassign from the extras. |

## Direct DB checks (`hiveiq_fleet`)

```sql
-- window definition
SELECT id, institution_key, name, days_of_week, week_of_month, start_time, end_time, timezone
FROM patch_windows WHERE id = ?;

-- devices attached to a window
SELECT atm_id, institution_key FROM patch_window_devices WHERE patch_window_id = ?;

-- which window(s) an ATM is in
SELECT * FROM patch_window_devices WHERE atm_id = '<AT…>';

-- scheduled installs waiting on a window
SELECT id, task_kind, status, scheduled_at FROM fleet_tasks
WHERE task_kind IN ('HOTFIX_INSTALL','SOFTWARE_INSTALL') AND scheduled_at IS NOT NULL;
```

> Postgres runs on the **database VM** (`10.10.10.188`) via ProxyJump, not the services VM — see the monorepo CLAUDE.md.
