Skip to content

Commit 536cfa6

Browse files
committed
Merge remote-tracking branch 'origin/release/3.0.0' into feature/docs-revisions
2 parents b8d0961 + 8566816 commit 536cfa6

63 files changed

Lines changed: 1988 additions & 2255 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS=300
180180
EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS=300
181181
###< Event Database Api V2 Feed Type ###
182182

183+
###> Instant Book ###
184+
# Source of busy intervals data for instant booking interactive slides (graph or feed).
185+
INSTANT_BOOK_BUSY_INTERVALS_SOURCE=graph
186+
###< Instant Book ###
187+
183188
###> Admin configuration ###
184189
# API key for Rejseplanen (Danish journey planner) integration.
185190
ADMIN_REJSEPLANEN_APIKEY=

.github/workflows/doctrine.yaml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,41 @@
33
### Runs pending Doctrine migrations and validates the resulting schema
44
### against the entity metadata.
55
###
6+
### #### MariaDB leg
7+
###
8+
### The supported, deployment-target leg. CI matrix runs the full
9+
### migrations:migrate + schema:validate cycle against MariaDB 10.11 and
10+
### 11.4. A green run here means upgrades will work in production.
11+
###
12+
### #### Postgres leg (EXPERIMENTAL — schema-portability gate only)
13+
###
14+
### NOT a claim that this project runs on Postgres. Postgres is NOT a
15+
### deployment target, NOT exercised by PHPUnit, and NOT covered by the
16+
### rest of CI. The leg exists only to catch regressions in the narrow
17+
### slice of portability we have so far achieved:
18+
###
19+
### - Entity metadata (column types, index names, quoted identifiers)
20+
### stays platform-neutral.
21+
### - The consolidated migration's up() emits portable DDL via Schema
22+
### tool API.
23+
###
24+
### Known gaps that still block real Postgres deployment:
25+
###
26+
### - Native MariaDB SQL in entity listeners (`MultiTenantRepositoryTrait`,
27+
### `RelationsChecksumCalculator`) — Postgres would fail at runtime.
28+
### - Doctrine cache, fixture loading, and most application queries are
29+
### untested against Postgres.
30+
###
31+
### A failure on this leg is a blocker — new schema changes must stay
32+
### portable; widening the MariaDB-only surface requires explicit review.
33+
###
634
### #### Assumptions
735
###
836
### 1. A docker compose service named `phpfpm` can be run and `bin/console`
937
### can be run inside the `phpfpm` service.
1038
### 2. A `mariadb` service is reachable from `phpfpm`.
39+
### 3. `docker-compose.postgres.yml` adds a `postgres` service and layers
40+
### `pdo_pgsql` onto `phpfpm`, used by the Postgres leg below.
1141

1242
on: pull_request
1343

@@ -55,3 +85,45 @@ jobs:
5585

5686
- name: Validate Doctrine schema
5787
run: docker compose run --rm -e APP_ENV=prod -e MARIADB_VERSION phpfpm bin/console doctrine:schema:validate
88+
89+
# EXPERIMENTAL: schema-portability regression gate only. Postgres is NOT
90+
# a supported deployment target — see the header comment at the top of
91+
# this file for the full caveat. A green run means entity metadata and
92+
# the consolidated migration's up() are portable; it does NOT mean the
93+
# application runs on Postgres.
94+
validate-doctrine-schema-postgres:
95+
runs-on: ubuntu-latest
96+
name: Validate Schema (postgres:16, experimental)
97+
env:
98+
# docker-compose.postgres.yml adds the postgres service and layers
99+
# pdo_pgsql onto phpfpm. COMPOSE_FILE stacks it on top of the default.
100+
COMPOSE_FILE: docker-compose.yml:docker-compose.postgres.yml
101+
DATABASE_URL: "postgresql://db:db@postgres:5432/db?serverVersion=16&charset=utf8"
102+
steps:
103+
- name: Checkout
104+
uses: actions/checkout@v6
105+
106+
- name: Cache vendor (prod)
107+
uses: actions/cache@v5
108+
with:
109+
path: vendor
110+
key: vendor-prod-php8.4-${{ hashFiles('composer.lock') }}
111+
restore-keys: vendor-prod-php8.4-
112+
113+
- name: Setup network
114+
run: docker network create frontend
115+
116+
# Build the phpfpm-with-pdo_pgsql image declared in
117+
# docker-compose.postgres.yml; without this, `docker compose run` reuses
118+
# the upstream itkdev/php8.4-fpm:latest image which lacks pdo_pgsql.
119+
- name: Build phpfpm with pdo_pgsql
120+
run: docker compose build phpfpm
121+
122+
- name: "[prod] Composer install"
123+
run: docker compose run --rm -e APP_ENV=prod phpfpm composer install --no-dev -o
124+
125+
- name: Run Doctrine Migrations
126+
run: docker compose run --rm -e APP_ENV=prod -e DATABASE_URL phpfpm bin/console doctrine:migrations:migrate --no-interaction
127+
128+
- name: Validate Doctrine schema
129+
run: docker compose run --rm -e APP_ENV=prod -e DATABASE_URL phpfpm bin/console doctrine:schema:validate

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
- Rewrote the consolidated end-of-2.8 migration to Doctrine's Schema tool API;
8+
added a `NoAddSqlInMigrationRule` PHPStan rule to enforce the convention on future migrations.
9+
- Added a Postgres `Validate Schema` job to the Doctrine workflow as a regression gate against
10+
entity/migration drift from Postgres compatibility; uses the new `docker-compose.postgres.yml` override.
11+
- Consolidated 25 historical 2.x Doctrine migrations into a single end-of-2.8 schema migration;
12+
upgraders run `doctrine:migrations:rollup` (see `UPGRADE.md` step 3).
13+
- Restored three deprecated `Template` properties (`icon`, `resources`, `description`) as
14+
write-only fields; scheduled for removal in 3.1.
15+
- Merged fixes from 2.7.0 into release/3.0.0.
16+
- Added `INSTANT_BOOK_BUSY_INTERVALS_SOURCE` to select between Graph and the slide's calendar feed as the source of busy
17+
intervals for InstantBook.
18+
- Changed polling interval for instant booking template.
719
- Fixed admin toast leaking a raw `SyntaxError: Unexpected token '<'` when an upload was rejected
820
upstream (e.g. nginx 413); the toast now shows `HTTP <status>` instead.
921
- Made the media upload max size configurable via the new `MEDIA_MAX_UPLOAD_SIZE_MB` env var.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,21 @@ EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS=300
680680
- EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS: What should the expiration be for cache entries in
681681
EventDatabaseApiV2FeedType?
682682

683+
#### InstantBook
684+
685+
```dotenv
686+
###> InstantBook ###
687+
INSTANT_BOOK_BUSY_INTERVALS_SOURCE=graph
688+
###< InstantBook ###
689+
```
690+
691+
- INSTANT_BOOK_BUSY_INTERVALS_SOURCE: Where the InstantBook interactive slide fetches resource
692+
busy-intervals from.
693+
- `graph`: Fetch busy intervals from Microsoft Graph (results cached for 15 minutes).
694+
- `feed`: Fetch busy intervals from the slide's configured calendar-output feed.
695+
696+
**Default**: `graph`.
697+
683698
## Rest API & Relationships
684699

685700
To avoid embedding all relations in REST representations but still allow the clients to minimize the amount of API calls

UPGRADE.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,36 @@ Rename the following .env variables in `.env.local`:
7373
- From `APP_KEY_VAULT_SOURCE` to `KEY_VAULT_SOURCE`
7474
- From `APP_KEY_VAULT_JSON` to `KEY_VAULT_JSON`
7575

76-
#### 3 - Run doctrine migrate
76+
#### 3 - Consolidate Doctrine migrations
77+
78+
3.0 ships a single consolidated migration that represents the end-of-2.8 schema. The 25 historical
79+
2.x migrations have been removed from the repository.
80+
81+
Because every upgrading database already matches that consolidated schema (via the 25 migrations it
82+
ran while on 2.x), there is nothing for `doctrine:migrations:migrate` to do — and running it would
83+
fail because of the orphaned version entries. Use `doctrine:migrations:rollup` instead, which
84+
truncates the `doctrine_migration_versions` table and inserts a single row marking the consolidated
85+
migration as already executed:
7786

7887
```shell
79-
docker compose exec phpfpm bin/console doctrine:migrations:migrate
88+
# Confirm the database is at the latest 2.8.x state before rolling up.
89+
# All 25 historical versions should appear as "migrated" / "available".
90+
docker compose exec phpfpm bin/console doctrine:migrations:status
91+
92+
# Replace the 25 historical version entries with the single consolidated entry.
93+
# This does not run any SQL — it only rewrites the version-tracking table.
94+
docker compose exec phpfpm bin/console doctrine:migrations:rollup --no-interaction
8095
```
8196

97+
> **Prerequisite:** the database must be on the final 2.8.x release with every 2.x migration
98+
> applied. If `doctrine:migrations:status` (run while still on 2.8.x) reports any pending
99+
> migrations, run `doctrine:migrations:migrate` on 2.8.x first, then upgrade to 3.0 and continue
100+
> here.
101+
102+
Fresh installs (no prior 2.x database) skip the rollup and run
103+
`doctrine:migrations:migrate` instead — it executes the single consolidated migration and brings
104+
the schema up in one step.
105+
82106
#### 4 - Run template list command to see status for installed templates
83107

84108
```shell

assets/shared/templates/brnd/brnd-idraetsanlaeg.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function BrndIdraetsanlaeg({
112112
rowIndex % 2 === 0 ? "row-even" : "row-odd"
113113
}`}
114114
>
115-
{getTitle(entry.area)}
115+
{getTitle(entry.facility)}
116116
</ContentItem>
117117
<ContentItem
118118
className={`content-item content-item-activity ${

assets/shared/templates/calendar/calendar-single-booking.jsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,21 @@ function CalendarSingleBooking({
6464
// Get values from client localstorage.
6565
const token = localStorage.getItem("apiToken");
6666
const tenantKey = localStorage.getItem("tenantKey");
67-
const apiUrl = localStorage.getItem("apiUrl");
6867

6968
const [bookableIntervals, setBookableIntervals] = useState([]);
7069
const [fetchingIntervals, setFetchingIntervals] = useState(false);
7170
const [currentTime, setCurrentTime] = useState(dayjs());
7271
const [bookingResult, setBookingResult] = useState(null);
7372
const [processingBooking, setProcessingBooking] = useState(false);
7473
const [secondsUntilNextEvent, setSecondsUntilNextEvent] = useState(null);
75-
const [bookingError, setBookingError] = useState(false);
74+
const [bookingError, setBookingError] = useState(null);
7675

7776
const fetchBookingIntervals = () => {
7877
if (!instantBookingEnabled) {
7978
return;
8079
}
8180

82-
if (!apiUrl || !slide || !token || !tenantKey) {
81+
if (!slide || !token || !tenantKey) {
8382
setFetchingIntervals(false);
8483
return;
8584
}
@@ -93,7 +92,7 @@ function CalendarSingleBooking({
9392
if (resources.length === 1) {
9493
setFetchingIntervals(true);
9594

96-
fetch(`${apiUrl}${slide["@id"]}/action`, {
95+
fetch(`${slide["@id"]}/action`, {
9796
method: "POST",
9897
headers: {
9998
authorization: `Bearer ${token}`,
@@ -173,7 +172,7 @@ function CalendarSingleBooking({
173172
};
174173

175174
const clickInterval = (interval) => {
176-
if (!apiUrl || !slide || !token || !tenantKey) {
175+
if (!slide || !token || !tenantKey) {
177176
return;
178177
}
179178

@@ -183,7 +182,7 @@ function CalendarSingleBooking({
183182

184183
setProcessingBooking(true);
185184

186-
fetch(`${apiUrl}${slide["@id"]}/action`, {
185+
fetch(`${slide["@id"]}/action`, {
187186
method: "POST",
188187
headers: {
189188
authorization: `Bearer ${token}`,
@@ -198,14 +197,21 @@ function CalendarSingleBooking({
198197
},
199198
}),
200199
})
201-
.then((r) => r.json())
200+
.then((r) => {
201+
if (!r.ok) {
202+
const error = new Error(`Booking failed with status ${r.status}`);
203+
error.status = r.status;
204+
throw error;
205+
}
206+
return r.json();
207+
})
202208
.then((data) => {
203209
setBookingResult(data);
204210
setInstantBookingFromLocalStorage(slide["@id"], data);
205211
})
206-
.catch(() => {
207-
setBookingError(true);
208-
setTimeout(() => setBookingError(false), 10000);
212+
.catch((err) => {
213+
setBookingError(err?.status === 409 ? "conflict" : "generic");
214+
setTimeout(() => setBookingError(null), 10000);
209215
})
210216
.finally(() => {
211217
setProcessingBooking(false);
@@ -217,7 +223,8 @@ function CalendarSingleBooking({
217223
dayjs.extend(localizedFormat);
218224

219225
intervalChecking();
220-
const interval = setInterval(intervalChecking, 5000);
226+
// Check every minute if the current time has changed.
227+
const interval = setInterval(intervalChecking, 60000);
221228

222229
return () => {
223230
if (interval !== null) {
@@ -385,7 +392,15 @@ function CalendarSingleBooking({
385392
/>
386393
</p>
387394
)}
388-
{bookingError && (
395+
{bookingError === "conflict" && (
396+
<p>
397+
<FormattedMessage
398+
id="instant_booking_conflict"
399+
defaultMessage="Straksbooking fejlede. Intervallet er optaget."
400+
/>
401+
</p>
402+
)}
403+
{bookingError && bookingError !== "conflict" && (
389404
<p>
390405
<FormattedMessage
391406
id="instant_booking_error"

assets/tests/template/template-calendar.spec.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,12 @@ test("calendar-0-single-booking: ui tests", async ({ page }) => {
233233
test("calendar-1-single-booking: ui tests", async ({ page }) => {
234234
await fixTime(page);
235235
await page.goto("/template/calendar-1-single-booking");
236-
await expect(page.getByText("Ledigt")).toHaveCount(1);
237-
await expect(page.getByText("Ledigt")).toBeVisible();
236+
await expect(page.locator(".room-info")).toBeVisible();
238237

239-
await page.waitForTimeout(5500);
238+
const status = page.locator(".status");
239+
await expect(status).toHaveText("Ledigt");
240+
241+
await page.clock.runFor(61000);
240242

241-
await expect(page.getByText("Optaget")).toHaveCount(1);
242-
await expect(page.getByText("Optaget")).toBeVisible();
243+
await expect(status).toHaveText("Optaget");
243244
});

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"doctrine/doctrine-migrations-bundle": "^3.1",
2222
"doctrine/orm": "^2.9",
2323
"gesdinet/jwt-refresh-token-bundle": "^1.0",
24-
"itk-dev/openid-connect-bundle": "^3.1",
24+
"itk-dev/openid-connect-bundle": "^4.1",
2525
"justinrainbow/json-schema": "^5.2",
2626
"kubawerlos/php-cs-fixer-custom-fixers": "^3.11",
2727
"lexik/jwt-authentication-bundle": "^2.14",
@@ -90,6 +90,7 @@
9090
},
9191
"autoload-dev": {
9292
"psr-4": {
93+
"App\\PhpStan\\": "tools/phpstan/",
9394
"App\\Tests\\": "tests/"
9495
}
9596
},

0 commit comments

Comments
 (0)