# Changelog All notable changes to this project are documented here. 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). ## [Unreleased] Nothing scheduled. ## [0.23.0] — 2026-05-07 Quality-of-life release on top of `v0.22.0`: explicit OIDC kill-switch for dev / testing, a production-bootstrap guard against shipping an unreachable instance, and a refreshed admin-only Runtime panel that surfaces app version + creator instead of the PHP version. ### Added - **`OIDC_ENABLED` kill-switch for dev / testing.** New env var (default `true`) that, when set to `false` / `0` / `no` / `off` (case-insensitive, trimmed), forces `OidcClient::isConfigured()` to return false even when every `ENTRA_*` var is populated. `/auth/login` and `/auth/callback` both short-circuit to the same operator-facing 503 config page, with copy that distinguishes "disabled by flag" from "not configured". Lets dev / on-prem deployments route everyone through `LOCAL_ADMIN_*` without unsetting the Entra creds in `.env`. New `OidcClient::isExplicitlyDisabled()` helper and 6 lock-in tests in `tests/Auth/OidcClientTest.php`. - **`App\Meta` — single source of truth for app version + creator.** New class exposes `Meta::VERSION` (`0.23.0`) and `Meta::CREATOR` (`Alessandro Chiapparini`); bump alongside the release commit so the CHANGELOG heading, the git tag, and the in-app Runtime panel stay aligned. ### Changed - **Production bootstrap refuses to start with no sign-in method.** `public/index.php` now aborts with a 503 + `Retry-After: 30` and an `error_log` line when `APP_ENV=production` and neither OIDC nor `LOCAL_ADMIN_*` is enabled. Stops a fully unreachable instance from shipping silently after a misconfigured deploy. - **Admin-only Runtime panel on `/` swaps contents.** Drops the `PHP` row; adds `App version` and `Creator` (sourced from `App\Meta`). The OIDC row's value vocabulary changes from `configured` / `not configured` to `enabled` / `disabled`, matching the Local admin row, so `OIDC_ENABLED=false` reads naturally. No leak-surface change — the gate (`currentUser is not null and currentUser.isAdmin` from R01-N02) is unchanged, and `TwigViewTest::testHomeForAnonymousUserHidesRuntimePanel` was updated to assert that neither the new `appVersion` nor `appCreator` strings render for anonymous visitors. ## [0.22.0] — 2026-05-07 First tagged release. The minor version mirrors the latest build phase shipped (Phase 22). Section grouping below reflects the build-phase history captured in `SPEC.md` §9 plus the security-review hardening pass tracked in `doc/REVIEW_01.md` (R01-N01..R01-N34). ### Added - **Phase 1 — Skeleton.** Front controller, router, request/response primitives, view layer. - **Phase 2 — Auth.** Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE), session + CSRF, first-user-is-admin bootstrap, optional local-admin fallback for dev / on-prem deployments. - **Phase 3 — Workers, sprints, audit.** Worker CRUD, sprint create, per-cell audit log. - **Phase 4 — Sprint settings.** Sprint metadata edit, weeks resize, worker membership add/remove/reorder, per-row reserve-time-buffer (RTB). - **Phase 5 — Arbeitstage grid.** Editable working-days matrix, capacity calculation, per-cell persistence with audit. - **Phase 6 — Task list.** CRUD, assignments grid, sort/filter/search, drag-reorder. - **Phase 7 — Audit viewer + polish.** `/audit` admin page with filters, pagination, collapsible diffs; security headers + strict-ish CSP; CSRF audit (18/18 mutations); PHPUnit harness with 59 tests. - **Phase 8 — Cascade audit integrity.** Three FK cascade paths (sprint_worker → sprint_worker_days, sprint_worker → task_assignments, sprint_week → sprint_worker_days on shrink) snapshot-audit before the parent delete fires. - **Phase 9 — Users management.** `GET /users` + `POST /users/{id}` with self-demote and last-admin guardrails. - **Phase 10 — Task list polish.** Multi-select owner filter and column visibility toggle, both pure client-side with localStorage persistence. - **Phase 11 — CSP hardening.** Vendored Tailwind via a Node css-builder Docker stage; inline `onclick` replaced by `data-href` + `app.js`; CSP dropped `unsafe-inline` and the Tailwind CDN host. - **Phase 12 — Per-week weekday selection (Mo–Fr) drives Arbeitstage.** `sprint_weeks.active_days_mask` becomes the source of truth; `max_working_days` is a cached `popcount(mask)` projection. Five checkboxes per week in Sprint Settings; row of five dots per week in the sprint view (green = active, gray = off). Migration 002 backfills legacy rows. - **Phase 13 — Focus filter + Reset in the task list.** New `[data-focus-select]` picks one sprint worker; `applyFocusColumnVisibility` hides all-zero columns; `[data-reset-filters]` wipes search, prio, ownerFilterSet, focusWorker, and hiddenCols in one click. State persists in localStorage. - **Phase 14 — Hamburger menu.** Workers / Users / Audit log / Sign out collapsed into a dropdown behind a `data-menu-trigger` button with an inline-SVG icon; vanilla-JS toggle (~30 lines) honours outside-click, Escape, and focus return. - **Phase 15 — Big-screen (beamer) task viewer.** New signed-in route `/sprints/{id}/present` renders a stripped-down view for projection; shared partial with `/sprints/{id}` via a `loadSprintPage` helper. Auto vertical column headers when overflow detected. - **Phase 16 — Dark-mode toggle + light-mode contrast cleanup.** Manual toggle (no `prefers-color-scheme` auto-detect); `theme-init.js` synchronously sets `` from localStorage to prevent FOUC; comprehensive `dark:` class sweep across every view. - **Phase 17 — Number-stepper popover (later replaced).** Hidden native number-input spinners (`@layer base` reset); per-cell click-to-open vertical-slider popover on day, RTB, and task-assignment inputs. After seven iterations the implementation was removed in favour of plain typed entry; ArrowUp/ArrowDown stepping retained via browser default. - **Phase 18 — Per-cell task-status colours + filter + global toggle.** `task_assignments.status` ∈ {`zugewiesen`, `gestartet`, `abgeschlossen`, `abgebrochen`}; new `app_settings` KV table with opt-in `task_status_enabled` flag; admin `/settings` page; Status multi-select filter; first non-admin write surface (`PATCH /tasks/{id}/assignments/status`). Migration 003. - **Phase 19 — Twig 3 + Tailwind 3 + Alpine CSP + htmx + SortableJS; jQuery removed.** Stack-shift of the entire UI layer with zero changes to controllers, repositories, schema, capacity math, or audit semantics. All 11 `views/*.php` rewritten as `*.twig`. Strict CSP: `script-src 'self'` and `style-src 'self'` only — no third-party hosts. - **Phase 20 — XLSX import wizard.** Two-step admin-only wizard at `/sprints/import`; multipart upload (≤ 5 MB, ZIP magic-byte check), preview screen with target picker (Create new / Merge into empty existing), per-sheet skip toggle, transactional commit; per-sprint `IMPORTED_FROM_XLSX` audit row. New service trio: `XlsxColorClassifier`, `XlsxSprintImporter`, `SprintImporter`. Composer dep `phpoffice/phpspreadsheet ^3.4`; runtime stage adds `zip` + `gd` extensions. - **Phase 21 — Auto-derived week count in sprint settings.** `PATCH /sprints/{id}` resyncs `sprint_weeks` whenever `start_date` or `end_date` changes (target = `floor((end − start)/7) + 1`, capped at 26). Existing rows realign; appended rows default to `MASK_ALL`; trailing rows shrink with the same audit-cascaded-days flow as the legacy `replaceWeeks`. - **Phase 22 — Per-task hamburger menu: move / copy / edit / reorder.** Per-row admin button gathers Edit details / Move to sprint / Copy to sprint / Move (pick up) / Delete. Tasks gain `description` (≤ 8000 chars), `url` (`http(s)://`, ≤ 2048 chars), and `linked_task_id` for bidirectional cross-sprint copy chips. Two new admin JSON endpoints (`POST /tasks/{id}/move`, `POST /tasks/{id}/copy`). Migration 004. - **Sprint settings: secured Delete sprint action.** Danger-zone `POST /sprints/{id}/delete` form gated by `requireAdmin` + CSRF + typed-confirmation match; full-cascade audit (every descendant snapshotted) plus an UPDATE audit for cross-sprint linked rows whose `linked_task_id` was nulled out. ### Changed - **Header cleanup: Import moved into the admin dropdown.** The `/sprints/import` link moved from inline in the header to the top of the admin section in the hamburger menu. - **New sprint form: drop weeks input.** `/sprints/new` no longer collects `n_weeks`; the count is derived from `start_date` / `end_date` (capped at 26; above that redirects with `?error=dates_too_long`). - **Task table polish.** Sortable headers gain `whitespace-nowrap`; per-row assignment cells gain `whitespace-nowrap`; `.assign-status-*` tint moved off the `