Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 11 additions & 7 deletions .github/workflows/plugins-ci.yml
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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: |
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/`, 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`.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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

---
Expand Down
2 changes: 1 addition & 1 deletion design-system/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docker-compose.stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions includes/listmonk.php
Original file line number Diff line number Diff line change
Expand Up @@ -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-<id> 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
Expand Down Expand Up @@ -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;
}
}
}
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion pages/rss.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']));
Expand Down
4 changes: 2 additions & 2 deletions plugins/README.html
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,7 @@ <h3>CSS</h3>

<h2 id="dev">Local development</h2>

<p>Use the <strong>Docker setup</strong> 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.</p>
<p>Use the <strong>Docker setup</strong> 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.</p>

<pre><code>docker compose up --build</code></pre>

Expand All @@ -1239,7 +1239,7 @@ <h2 id="tests">Tests &amp; CI</h2>
<tr><td>Front-end</td><td><code>"build": {…}</code></td><td>install → lint → test → build, per plugin</td></tr>
<tr><td>End-to-end</td><td><code>"tests": {"e2e": "e2e"}</code></td><td><code>e2e/*.spec.ts</code> (Playwright), importing <code>test</code> from <code>tests/e2e/fixtures.ts</code></td></tr>
</table>
<p>Every PHP file also goes through <code>php -l</code> (PHP 7.4). The end-to-end job starts <code>docker-compose.stack.yml</code>
<p>Every PHP file also goes through <code>php -l</code> (PHP 8.4). The end-to-end job starts <code>docker-compose.stack.yml</code>
(site, Keycloak, decks API) with the built plugins and runs <code>tests/e2e</code> plus every plugin's <code>e2e/</code>,
at 1440×900 and 390×844. The check to require is <strong>Plugins CI</strong>.</p>
<pre><code>php bin/plugins.php plan
Expand Down
2 changes: 1 addition & 1 deletion plugins/equinox-deck-import/src/Domain/DeckParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private function accumulateDecks(string $raw): array
continue;
}

$cols = str_getcsv($line, ';', '"');
$cols = str_getcsv($line, ';', '"', '\\');
if (count($cols) < 8) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private function request(string $method, string $path, ?string $payload = null):

/**
* @param string[] $ids
* @return array<string, resource>
* @return array<string, resource|\CurlHandle>
*/
private function openHandles($multi, array $ids): array
{
Expand Down Expand Up @@ -138,7 +138,7 @@ private function runMulti($multi): void
}

/**
* @param array<string, resource> $handles
* @param array<string, resource|\CurlHandle> $handles
* @param array<string,mixed> $errors
* @return array<string, array>
*/
Expand Down
2 changes: 1 addition & 1 deletion plugins/reunion-events/README.html
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ <h3>CSS</h3>

<h2 id="dev">Local development</h2>

<p>Use the <strong>Docker setup</strong> 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.</p>
<p>Use the <strong>Docker setup</strong> 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.</p>

<pre><code>docker compose up --build</code></pre>

Expand Down
24 changes: 12 additions & 12 deletions themes/azure/nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? '');
Expand Down Expand Up @@ -76,10 +76,10 @@ function __nav_active(array $item, string $currentPage, int $iframeNavId): bool
<div class="az-navband-inner">
<ul class="navbar-nav az-nav-list d-flex flex-row">
<?php foreach ($__navItems as $__ni):
$__niActive = empty($__ni['is_sidebar_toggle']) && __nav_active($__ni, $currentPage, $__iframeNavId);
$__niActive = empty($__ni['is_sidebar_toggle']) && acNavActive($__ni, $currentPage, $__iframeNavId);
if (!$__niActive && !empty($__ni['children'])) {
foreach ($__ni['children'] as $__nc) {
if (__nav_active($__nc, $currentPage, $__iframeNavId)) {
if (acNavActive($__nc, $currentPage, $__iframeNavId)) {
$__niActive = true; break;
}
}
Expand Down Expand Up @@ -132,8 +132,8 @@ class="nav-link"
<div class="az-mega-title"><?= h($__col['title']) ?></div>
<?php endif; ?>
<?php foreach ($__col['items'] as $__nc): ?>
<a class="dropdown-item az-mega-link <?= __nav_active($__nc, $currentPage, $__iframeNavId) ? 'active' : '' ?>"
href="<?= h(__nav_href($__nc)) ?>"
<a class="dropdown-item az-mega-link <?= acNavActive($__nc, $currentPage, $__iframeNavId) ? 'active' : '' ?>"
href="<?= h(acNavHref($__nc)) ?>"
<?= (!empty($__nc['is_blank']) && empty($__nc['is_iframe'])) ? 'target="_blank" rel="noopener noreferrer"' : '' ?>>
<?php if (!empty($__nc['icon'])): ?>
<span class="az-mega-icon"><?= ac_icon((string)$__nc['icon']) ?></span>
Expand All @@ -153,7 +153,7 @@ class="nav-link"
</li>
<?php else: ?>
<li class="nav-item">
<a href="<?= h(__nav_href($__ni)) ?>"
<a href="<?= h(acNavHref($__ni)) ?>"
class="nav-link <?= $__niActive ? 'active' : '' ?>"
<?= (!empty($__ni['is_blank']) && empty($__ni['is_iframe'])) ? 'target="_blank" rel="noopener noreferrer"' : '' ?>
title="<?= h($__ni['label']) ?>">
Expand Down Expand Up @@ -185,7 +185,7 @@ class="nav-link <?= $__niActive ? 'active' : '' ?>"
<?php if ($__mmLinks): ?>
<div class="az-mm-links">
<?php foreach ($__mmLinks as $__ni): ?>
<a class="az-mm-pill<?= __nav_active($__ni, $currentPage, $__iframeNavId) ? ' active' : '' ?>" href="<?= h(__nav_href($__ni)) ?>"
<a class="az-mm-pill<?= acNavActive($__ni, $currentPage, $__iframeNavId) ? ' active' : '' ?>" href="<?= h(acNavHref($__ni)) ?>"
<?= (!empty($__ni['is_blank']) && empty($__ni['is_iframe'])) ? 'target="_blank" rel="noopener noreferrer"' : '' ?>>
<?= !empty($__ni['icon']) ? ac_icon((string)$__ni['icon']) : '' ?>
<span><?= h($__ni['label']) ?></span>
Expand All @@ -199,8 +199,8 @@ class="nav-link <?= $__niActive ? 'active' : '' ?>"
<div class="az-mm-tiles">
<?php foreach ($__ni['children'] as $__nc):
if (!empty($__nc['is_section_header']) || !empty($__nc['is_separator'])) continue; ?>
<a class="az-mm-tile<?= __nav_active($__nc, $currentPage, $__iframeNavId) ? ' active' : '' ?>"
href="<?= h(__nav_href($__nc)) ?>"
<a class="az-mm-tile<?= acNavActive($__nc, $currentPage, $__iframeNavId) ? ' active' : '' ?>"
href="<?= h(acNavHref($__nc)) ?>"
<?= (!empty($__nc['is_blank']) && empty($__nc['is_iframe'])) ? 'target="_blank" rel="noopener noreferrer"' : '' ?>>
<?= !empty($__nc['icon']) ? ac_icon((string)$__nc['icon']) : '' ?>
<span><?= h($__nc['label']) ?></span>
Expand Down
Loading