diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 505d99d8..57caed58 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,7 +35,7 @@ jobs: # dossier source (workdir, node_modules compris) pour n'envoyer que la sortie. - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.4' coverage: none - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/plugins-ci.yml b/.github/workflows/plugins-ci.yml index 619bbbdc..f8635661 100644 --- a/.github/workflows/plugins-ci.yml +++ b/.github/workflows/plugins-ci.yml @@ -1,7 +1,7 @@ name: Plugins CI # Checks every pull request against preprod / main, separately from deploy.yml: -# • php -l on all PHP (7.4, the production version); +# • php -l on all PHP (8.4, the production version; 7.4 kept until preprod has switched); # • each plugin's tests, found through bin/plugins.php (plugin.json "tests" / "build", # or the conventions tests/run.php, **/*.test.mjs, e2e/*.spec.ts); # • front-end plugins ("build" in plugin.json): install, lint, unit tests, build; @@ -40,7 +40,7 @@ jobs: fetch-depth: 2 # the pull request's merge commit and its first parent, the base branch - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.4' coverage: none - id: plan env: @@ -92,14 +92,18 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" php: - name: PHP lint and plugin PHP tests + name: PHP lint and plugin PHP tests (${{ matrix.php }}) needs: plan runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.4'] # 7.4: preprod runtime until its switch — drop after it steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: ${{ matrix.php }} coverage: none - name: php -l run: | @@ -125,7 +129,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.4' coverage: none - uses: actions/setup-node@v4 with: @@ -149,7 +153,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.4' coverage: none - uses: actions/setup-node@v4 with: @@ -188,7 +192,7 @@ jobs: run: $COMPOSE up -d keycloak db decks-db - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.4' coverage: none - uses: actions/setup-node@v4 with: diff --git a/AGENTS.md b/AGENTS.md index f89abc77..6b886da8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Instructions for coding agents (Codex, Cursor, Copilot, Claude Code…). Claude Code reads them through `CLAUDE.md`, which imports this file. -PHP 7.4 site (no framework, Bootstrap 5 from a CDN) with a plugin system: PHP plugins +PHP 8.4 site (no framework, Bootstrap 5 from a CDN) with a plugin system: PHP plugins (`plugins//`, manifest `plugin.json`) and SPA plugins (manifest v2, `"type": "spa"`, mounted in a Shadow DOM, contract `window.AlteredCore` in `js/altered-core-host.js`). See `README.md`, `CONTRIBUTING.md` and `plugins/README.html`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5deec05..46eafd38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,7 +109,7 @@ git push origin dev:main ## Code conventions -- **PHP 7.4 only.** The server runs PHP 7.4. +- **PHP 8.4.** Production runs PHP 8.4.27 (Docker image php:8.4.27-apache). Until preprod has switched too, the preprod CI also lints under 7.4; keep new code free of PHP 8.5-only features. - **No framework, no Composer.** No build step, no external dependencies. The project is intentionally plain PHP — keep it that way. - **Comments in English.** - **Interface: the design system.** Every screen uses `design-system/` (read its `README.md`): colours, sizes, radii and shadows come from the `--ac-*` tokens (`design-system/tokens/tokens.css`, the only file with raw values), components are the `ac-*` classes, icons are Lucide (`ac_icon('name')`). Light/dark and pointer/touch are handled by the tokens: no `[data-theme]` rule in page, theme or plugin CSS. CSS files: (1) `design-system/`: tokens and shared components; (2) `css/style.css`: shell and core pages; (3) `themes/azure/`: header, menu and footer templates and their CSS; (4) `plugins/*/assets/*.css`: plugin-specific layout, built from tokens. `php tests/run.php` rejects hard-coded colours. Designing with Claude (`/design`): `design-system/WORKFLOW.md`. diff --git a/Dockerfile b/Dockerfile index 5faa6da8..5a678926 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ -FROM php:7.4-apache +FROM php:8.4.27-apache -# php:7.4 is Debian 11 (bullseye), whose LTS ended in August 2026: the bullseye-security -# mirror now 404s on package files and breaks apt-get. Install from bullseye main only. -RUN sed -i '/bullseye-security/d' /etc/apt/sources.list \ - && apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y \ libpng-dev \ libjpeg62-turbo-dev \ libwebp-dev \ diff --git a/README.md b/README.md index 4cec139f..16c2f6db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Unofficial community website for the [Altered TCG](https://www.altered.gg/) card game. -Built with plain PHP — no framework, no build step, no Composer. Runs on any standard shared hosting with Apache and PHP 7.4. +Built with plain PHP — no framework, no build step, no Composer. Runs on any standard shared hosting with Apache and PHP 8.4. --- @@ -17,7 +17,7 @@ Pull requests that introduce external dependencies or non-standard server requir ## Requirements - Apache with `mod_rewrite` enabled -- PHP 7.4 +- PHP 8.4 (8.2 and 8.3 also run the code; 7.4 is no longer supported once both environments have switched) - MariaDB 10.x --- diff --git a/design-system/AGENTS.md b/design-system/AGENTS.md index 9d319dfb..c56fe20d 100644 --- a/design-system/AGENTS.md +++ b/design-system/AGENTS.md @@ -19,6 +19,6 @@ before changing anything under `design-system/` or building a screen. or the script, then `npm ci && npm run build` in `design-system/icons/`, never the outputs. - Bridges (`css/bridges/`) exist for pages not migrated yet. Do not add new legacy names; when a page is migrated, it uses `ac-*` classes and `--ac-*` tokens directly. -- PHP helpers (`php/ui.php`) must stay PHP 7.4 compatible (production runs php:7.4). +- PHP helpers (`php/ui.php`) must stay PHP 7.4 compatible until preprod has switched to PHP 8.4 (production runs php:8.4.27). - After a change: `php tests/run.php` (Docker: `docker compose exec web php tests/run.php`), then look at `/pages/design-system` in both themes. diff --git a/docker-compose.stack.yml b/docker-compose.stack.yml index 6632d365..1ad1d3a2 100644 --- a/docker-compose.stack.yml +++ b/docker-compose.stack.yml @@ -38,7 +38,7 @@ services: # Stand-in for AlteredOwnership (alt-art families, preferences, preference mode), reached by the # site only (OWNERSHIP_API_URL); built on the production cards API. ownership: - image: php:8.3-cli-alpine + image: php:8.4-cli-alpine command: ["php", "-S", "0.0.0.0:8080", "/app/index.php"] volumes: - ./docker/stack/ownership-mock:/app:ro diff --git a/includes/listmonk.php b/includes/listmonk.php index d2c32553..57b8f6b2 100644 --- a/includes/listmonk.php +++ b/includes/listmonk.php @@ -129,7 +129,7 @@ function listmonkSubscribe(string $email, string $name, string $lang, array $att * campaigns:get_all / campaigns:manage_all permissions. * * Campaigns are found by their tags (news- and the language), not by name. - * The name carries the newsletter number, "Newsletter #N (FR) – title": N counts + * The name ends with the newsletter number, "title – Newsletter #N": N counts * the news whose e-mail was sent (running, paused or finished campaign), so the * many drafts never sent do not raise it, and both languages of one news share * it. A draft takes the next number; a news already sent in one language keeps @@ -164,7 +164,8 @@ function listmonkSaveNewsCampaign(int $newsId, string $lang, string $title, int if (in_array($c['status'] ?? '', $sent, true)) { foreach ($tags as $t) { if (strpos($t, 'news-') === 0) { - $sentNews[$t] = preg_match('/^Newsletter #(\d+)/', $c['name'] ?? '', $m) ? (int)$m[1] : 0; + // Last "Newsletter #N" of the name (a title may contain one too). + $sentNews[$t] = preg_match('/.*Newsletter #(\d+)/s', $c['name'] ?? '', $m) ? (int)$m[1] : 0; } } } @@ -175,7 +176,7 @@ function listmonkSaveNewsCampaign(int $newsId, string $lang, string $title, int $number = !empty($sentNews[$tag]) ? $sentNews[$tag] : count(array_diff_key($sentNews, [$tag => 0])) + 1; $payload = [ - 'name' => 'Newsletter #' . $number . ' (' . strtoupper($lang) . ') – ' . $title, + 'name' => $title . ' – Newsletter #' . $number, 'subject' => $title, 'lists' => [$listId], 'type' => 'regular', diff --git a/pages/rss.php b/pages/rss.php index 6e24d8a5..a8b99ce0 100644 --- a/pages/rss.php +++ b/pages/rss.php @@ -61,7 +61,7 @@ : $plain; } - $link = $siteUrl . '/pages/news-detail?slug=' . rawurlencode($news['slug']); + $link = $siteUrl . '/pages/news-detail?slug=' . rawurlencode((string) ($news['slug'] ?? '')); $pubDate = !empty($news['published_at']) ? date('r', strtotime($news['published_at'])) : date('r', strtotime($news['created_at'])); diff --git a/plugins/README.html b/plugins/README.html index 01e60399..14e9db11 100644 --- a/plugins/README.html +++ b/plugins/README.html @@ -1219,7 +1219,7 @@

CSS

Local development

-

Use the Docker setup included in the repository root. It runs the full site locally — Apache, PHP 7.4, MariaDB, and all plugins — so your plugin pages, API endpoints, and admin sections behave exactly as they would in production.

+

Use the Docker setup included in the repository root. It runs the full site locally — Apache, PHP 8.4, MariaDB, and all plugins — so your plugin pages, API endpoints, and admin sections behave exactly as they would in production.

docker compose up --build
@@ -1239,7 +1239,7 @@

Tests & CI

Front-end"build": {…}install → lint → test → build, per plugin End-to-end"tests": {"e2e": "e2e"}e2e/*.spec.ts (Playwright), importing test from tests/e2e/fixtures.ts -

Every PHP file also goes through php -l (PHP 7.4). The end-to-end job starts docker-compose.stack.yml +

Every PHP file also goes through php -l (PHP 8.4). The end-to-end job starts docker-compose.stack.yml (site, Keycloak, decks API) with the built plugins and runs tests/e2e plus every plugin's e2e/, at 1440×900 and 390×844. The check to require is Plugins CI.

php bin/plugins.php plan
diff --git a/plugins/equinox-deck-import/src/Domain/DeckParser.php b/plugins/equinox-deck-import/src/Domain/DeckParser.php
index 777b5142..b8cc80a7 100644
--- a/plugins/equinox-deck-import/src/Domain/DeckParser.php
+++ b/plugins/equinox-deck-import/src/Domain/DeckParser.php
@@ -44,7 +44,7 @@ private function accumulateDecks(string $raw): array
                 continue;
             }
 
-            $cols = str_getcsv($line, ';', '"');
+            $cols = str_getcsv($line, ';', '"', '\\');
             if (count($cols) < 8) {
                 continue;
             }
diff --git a/plugins/equinox-deck-import/src/Infrastructure/CurlDeckApiClient.php b/plugins/equinox-deck-import/src/Infrastructure/CurlDeckApiClient.php
index 53e47238..d979c0d6 100644
--- a/plugins/equinox-deck-import/src/Infrastructure/CurlDeckApiClient.php
+++ b/plugins/equinox-deck-import/src/Infrastructure/CurlDeckApiClient.php
@@ -101,7 +101,7 @@ private function request(string $method, string $path, ?string $payload = null):
 
     /**
      * @param string[] $ids
-     * @return array
+     * @return array
      */
     private function openHandles($multi, array $ids): array
     {
@@ -138,7 +138,7 @@ private function runMulti($multi): void
     }
 
     /**
-     * @param array $handles
+     * @param array $handles
      * @param array     $errors
      * @return array
      */
diff --git a/plugins/reunion-events/README.html b/plugins/reunion-events/README.html
index a0c10a68..a5b6f71e 100644
--- a/plugins/reunion-events/README.html
+++ b/plugins/reunion-events/README.html
@@ -928,7 +928,7 @@ 

CSS

Local development

-

Use the Docker setup included in the repository root. It runs the full site locally — Apache, PHP 7.4, MariaDB, and all plugins — so your plugin pages, API endpoints, and admin sections behave exactly as they would in production.

+

Use the Docker setup included in the repository root. It runs the full site locally — Apache, PHP 8.4, MariaDB, and all plugins — so your plugin pages, API endpoints, and admin sections behave exactly as they would in production.

docker compose up --build
diff --git a/themes/azure/nav.php b/themes/azure/nav.php index b57e95a0..0141d48a 100644 --- a/themes/azure/nav.php +++ b/themes/azure/nav.php @@ -5,16 +5,16 @@ // $_langNames, $_langUrls, $_hTxt, $__mobileCompact $__iframeNavId = ($currentPage === 'iframe') ? (int)($_GET['nav'] ?? 0) : 0; -if (!function_exists('__nav_href')) { - function __nav_href(array $item): string { +if (!function_exists('acNavHref')) { + function acNavHref(array $item): string { if (!empty($item['is_iframe'])) { return BASE_URL . '/pages/iframe?nav=' . (int)$item['id']; } return BASE_URL . resolveUrlLang($item['url']); } } -if (!function_exists('__nav_active')) { - function __nav_active(array $item, string $currentPage, int $iframeNavId): bool { +if (!function_exists('acNavActive')) { + function acNavActive(array $item, string $currentPage, int $iframeNavId): bool { if (!empty($item['is_separator']) || !empty($item['is_section_header'])) return false; if (!empty($item['is_iframe'])) return $iframeNavId === (int)$item['id']; $__parsed = parse_url($item['url'] ?? ''); @@ -76,10 +76,10 @@ function __nav_active(array $item, string $currentPage, int $iframeNavId): bool
- > @@ -153,7 +153,7 @@ class="nav-link"