Browse Source

Merge branch 'master' of https://github.com/unacms/UNA

AntonLV 2 weeks ago
parent
commit
1121cdc008

+ 26 - 16
.github/workflows/ci.yml

@@ -98,6 +98,7 @@ jobs:
       RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
       RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
       RAILWAY_BASE_ENVIRONMENT: ${{ secrets.RAILWAY_ENVIRONMENT }}
+      RAILWAY_TEMPLATE_ENVIRONMENT: pr-template
       RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
     steps:
     - uses: actions/checkout@v4
@@ -128,25 +129,34 @@ jobs:
           exit 1
         fi
         base="${RAILWAY_BASE_ENVIRONMENT:-production}"
+        template="${RAILWAY_TEMPLATE_ENVIRONMENT:-pr-template}"
         railway link --project "$RAILWAY_PROJECT_ID" --environment "$base" --json
-        environments=$(railway environment list --json)
-        if printf '%s\n' "$environments" | jq -e --arg name "$RAILWAY_PR_ENVIRONMENT" '.environments[] | select(.name == $name)' >/dev/null; then
-          echo "Reusing Railway environment $RAILWAY_PR_ENVIRONMENT"
-        else
-          railway environment new "$RAILWAY_PR_ENVIRONMENT" --duplicate "$base" --json
+
+        # Railway services are project-scoped: mariadb and una already exist, so `railway add`
+        # cannot create them again and a plain new environment holds no instances of them
+        # (every preview after the first failed this way). A PR environment therefore gets
+        # its instances by duplicating the template environment. An environment left over
+        # from a failed run has no una instance; recreate it.
+        if ! railway service status --service una --environment "$RAILWAY_PR_ENVIRONMENT" >/dev/null 2>&1; then
+          railway environment delete "$RAILWAY_PR_ENVIRONMENT" -y >/dev/null 2>&1 || true
+          railway environment new "$RAILWAY_PR_ENVIRONMENT" --duplicate "$template" --json
+          # a duplicate may carry the template's una deployment; the artefact below replaces it
+          railway down --service una --environment "$RAILWAY_PR_ENVIRONMENT" -y >/dev/null 2>&1 || true
         fi
         railway link --project "$RAILWAY_PROJECT_ID" --environment "$RAILWAY_PR_ENVIRONMENT" --json
-        railway variable set --service mariadb --environment "$RAILWAY_PR_ENVIRONMENT" --skip-deploys \
-          MARIADB_ROOT_PASSWORD=una \
-          MARIADB_DATABASE=una \
-          MARIADB_USER=una \
-          MARIADB_PASSWORD=una
-        railway variable set --service una --environment "$RAILWAY_PR_ENVIRONMENT" --skip-deploys \
-          MYSQLHOST=mariadb.railway.internal \
-          MYSQLPORT=3306 \
-          MYSQLDATABASE=una \
-          MYSQLUSER=una \
-          MYSQLPASSWORD=una
+
+        # The duplicated una instance is what `railway up` deploys into. Setting its variables
+        # doubles as the readiness check: it fails with "ServiceInstance not found" until it exists.
+        for i in $(seq 1 12); do
+          railway variable set --service una --environment "$RAILWAY_PR_ENVIRONMENT" --skip-deploys \
+            MYSQLHOST=mariadb.railway.internal \
+            MYSQLPORT=3306 \
+            MYSQLDATABASE=una \
+            MYSQLUSER=una \
+            MYSQLPASSWORD=una \
+            && break
+          sleep 5
+        done
         railway domain --service una --environment "$RAILWAY_PR_ENVIRONMENT" || true
 
     - name: Deploy artefact

+ 33 - 4
.htaccess

@@ -12,10 +12,39 @@ Options -MultiViews -Indexes
     RewriteRule ^page/(.*)$  page.php?i=$1 [QSA,L]
     RewriteRule ^s/([a-zA-Z0-9_]+)/([a-zA-Z0-9\.]+)  storage.php?o=$1&f=$2   [QSA,L]
 
-    RewriteCond %{REQUEST_FILENAME} -f [OR]
-    RewriteCond %{REQUEST_FILENAME} -d
-    RewriteRule .+ - [L]
-    RewriteRule ^(.+)$ r.php?_q=$1 [QSA,L]
+    RewriteCond %{REQUEST_FILENAME} -f [OR]
+    RewriteCond %{REQUEST_FILENAME} -d
+    RewriteRule .+ - [L]
+    RewriteRule ^(.+)$ r.php?_q=$1 [QSA,L]
 </IfModule>
 
 AddType image/svg+xml svg
+
+# Compress text responses (guarded: no effect unless mod_deflate is enabled).
+<IfModule mod_deflate.c>
+    AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml application/javascript text/javascript application/json application/xml image/svg+xml application/manifest+json
+</IfModule>
+
+# Long-lived caching for static assets (guarded: no effect unless mod_expires is enabled).
+# text/html is intentionally omitted so dynamic pages are never cached.
+<IfModule mod_expires.c>
+    ExpiresActive On
+    ExpiresByType text/css                  "access plus 30 days"
+    ExpiresByType application/javascript    "access plus 30 days"
+    ExpiresByType text/javascript           "access plus 30 days"
+    ExpiresByType image/svg+xml             "access plus 30 days"
+    ExpiresByType image/png                 "access plus 30 days"
+    ExpiresByType image/jpeg                "access plus 30 days"
+    ExpiresByType image/gif                 "access plus 30 days"
+    ExpiresByType image/webp                "access plus 30 days"
+    ExpiresByType image/x-icon              "access plus 30 days"
+    ExpiresByType font/woff2                "access plus 365 days"
+    ExpiresByType font/woff                 "access plus 365 days"
+    ExpiresByType font/ttf                  "access plus 365 days"
+    ExpiresByType application/vnd.ms-fontobject "access plus 365 days"
+
+    # The service worker is generated by PHP as text/javascript and must stay fresh.
+    <Files "sw.js.php">
+        ExpiresActive Off
+    </Files>
+</IfModule>

+ 1 - 0
composer.json

@@ -125,6 +125,7 @@
         ],
         "relocate-ffmpeg": [
             "test -f ./plugins/ffmpeg/ffmpeg/ffmpeg-linux-x64 && mv ./plugins/ffmpeg/ffmpeg/ffmpeg-linux-x64 ./plugins/ffmpeg/ffmpeg.exe && rm -rf ./plugins/ffmpeg/ffmpeg || true",
+            "test -f ./plugins/ffmpeg/ffmpeg.exe && chmod 0755 ./plugins/ffmpeg/ffmpeg.exe || true",
             "grep -Fq \"this->headers['Content-MD5']\" plugins/tpyo/amazon-s3-php-class/S3.php || patch -p1 --batch < patches/plugins_tpyo_amazon-s3-php-class.txt"
         ],
         "test": "phpunit -c tests/phpunit.xml",

+ 1 - 0
docker-compose.yml

@@ -19,6 +19,7 @@ services:
             dockerfile: scripts/docker-compose/PHP.Dockerfile
         volumes:
             - ./scripts/docker-compose/php.ini:/usr/local/etc/php/php.ini
+            - ./scripts/docker-compose/php-fpm-pool.conf:/usr/local/etc/php-fpm.d/zz-una.conf
             - ./:/opt/una
         extra_hosts:
             - "host.docker.internal:host-gateway"

+ 24 - 3
inc/classes/BxDolCacheFile.php

@@ -78,7 +78,7 @@ class BxDolCacheFile extends BxDolCache
     function delData($sKey)
     {
         $sFile = $this->sPath . $sKey;
-        return !file_exists($sFile) || @unlink($sFile);
+        return !file_exists($sFile) || $this->_unlinkFile($sFile);
     }
 
     /**
@@ -93,7 +93,7 @@ class BxDolCacheFile extends BxDolCache
         $l = strlen($s);
         while (($sFile = readdir($rHandler)) !== false)
             if (0 === strncmp($sFile, $s, $l))
-                @unlink ($this->sPath . $sFile);
+                $this->_unlinkFile($this->sPath . $sFile);
 
         closedir($rHandler);
 
@@ -129,12 +129,33 @@ class BxDolCacheFile extends BxDolCache
     {
         $iTimeDiff = time() - filectime($sFile);
         if ($iTimeDiff > $iTTL) {
-            @unlink ($sFile);
+            $this->_unlinkFile($sFile);
             return true;
         } else {
             return false;
         }
     }
+
+    /**
+     * Remove a cache file together with its opcache entry.
+     *
+     * Cache files are PHP scripts read with include(), so opcache keeps a compiled copy of each of them.
+     * It never evicts entries on its own, and once a file is gone opcache_invalidate() can no longer resolve
+     * its path, so a file removed with a plain unlink() leaves a dead entry behind for good. With hash-named
+     * cache files being regenerated all the time such entries pile up until the cache is full and every
+     * script that no longer fits is recompiled on every request. Invalidating before the unlink turns the
+     * entry into wasted memory instead, which opcache reclaims with its next restart.
+     *
+     * @param  string $sFile - full path to the file
+     * @return true if the file is deleted, false otherwise
+     */
+    protected function _unlinkFile ($sFile)
+    {
+        if (function_exists('opcache_invalidate'))
+            opcache_invalidate($sFile, true);
+
+        return @unlink($sFile);
+    }
 }
 
 /** @} */

+ 5 - 0
inc/classes/BxDolImageResize.php

@@ -138,6 +138,11 @@ class BxDolImageResize extends BxDolFactory implements iBxDolSingleton
         $this->_iJpegQuality = $i;
     }
 
+    public function getJpegQuality ()
+    {
+        return $this->_iJpegQuality;
+    }
+
     function setSquareResize ($b)
     {
         $this->_isSquareResize = ($b ? true : false);

+ 9 - 0
inc/classes/BxDolStorage.php

@@ -772,6 +772,15 @@ abstract class BxDolStorage extends BxDolFactory implements iBxDolFactoryObject
      */
     public function getFileUrlById($iFileId) { }
 
+    /**
+     * Get the absolute local filesystem path of a stored file. Only local storage
+     * engines can answer this; other engines return false and callers fall back to
+     * the file URL.
+     * @param $mixedHandler file id or remote id
+     * @return string absolute path on success, false otherwise
+     */
+    public function getFileLocalPath($mixedHandler) { return false; }
+
     /**
      * Get file info array by file id.
      * @param $iFileId file id

+ 16 - 0
inc/classes/BxDolStorageLocal.php

@@ -131,6 +131,22 @@ class BxDolStorageLocal extends BxDolStorage
         return true;
     }
 
+    /**
+     * Get the absolute local filesystem path of a stored file, or false when it is
+     * not available on disk.
+     * @param $mixedHandler file id or remote id
+     * @return string absolute path on success, false otherwise
+     */
+    public function getFileLocalPath ($mixedHandler)
+    {
+        $aFile = $this->getFile($mixedHandler);
+        if (!$aFile || empty($aFile['path']))
+            return false;
+
+        $sPath = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+        return file_exists($sPath) ? $sPath : false;
+    }
+
     protected function getObjectBaseDir ($isPrivate = false)
     {
         return BX_DIRECTORY_STORAGE . $this->_aObject['object'] . '/';

+ 25 - 2
inc/classes/BxDolTranscoder.php

@@ -720,7 +720,20 @@ class BxDolTranscoder extends BxDolFactory implements iBxDolFactoryObject
         else
             $o->setSquareResize (false);
 
-        if (IMAGE_ERROR_SUCCESS == $o->resize($sFile))
+        // Optional per-transcoder JPEG quality. BxDolImageResize is a singleton reused for every
+        // image in the request, so the previous quality is restored afterwards.
+        $iQualityPrev = null;
+        if (isset($aParams['quality']) && (int)$aParams['quality'] > 0) {
+            $iQualityPrev = $o->getJpegQuality();
+            $o->setJpegQuality((int)$aParams['quality']);
+        }
+
+        $iResult = $o->resize($sFile);
+
+        if ($iQualityPrev !== null)
+            $o->setJpegQuality($iQualityPrev);
+
+        if (IMAGE_ERROR_SUCCESS == $iResult)
             return true;
 
         bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: applyFilter_Resize failed for file ({$sFile}): " . $o->getError(), BX_LOG_ERR);
@@ -847,6 +860,17 @@ class BxDolTranscoder extends BxDolFactory implements iBxDolFactoryObject
             return false;
         }
 
+        $sTmpFile = $this->getTmpFilename ($aFile['file_name']);
+
+        // Prefer reading a local-engine source straight from disk. It avoids a
+        // self-referential HTTP round trip for every transcode (a small speed-up),
+        // and it works where the app cannot reach its own public URL from inside
+        // (e.g. a container whose BX_DOL_URL_ROOT is not routable locally).
+        $sLocalPath = $oStorageOriginal->getFileLocalPath($mixedHandler);
+        if ($sLocalPath && @copy($sLocalPath, $sTmpFile))
+            return $sTmpFile;
+
+        // Fallback: fetch over HTTP (remote storage engines, or if the disk copy failed).
         $sUrl = $oStorageOriginal->getFileUrlById($mixedHandler);
         if (!$sUrl) {
             bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, file({$mixedHandler}) wasn't found in source storage ({$this->_aObject['source_params']['object']})", BX_LOG_ERR);
@@ -859,7 +883,6 @@ class BxDolTranscoder extends BxDolFactory implements iBxDolFactoryObject
             return false;
         }
 
-        $sTmpFile = $this->getTmpFilename ($aFile['file_name']);
         if (!file_put_contents($sTmpFile, $sFileData)) {
             bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, file save failed ({$sTmpFile})", BX_LOG_ERR);
             return false;

+ 2 - 2
install/sql/system.sql

@@ -5960,8 +5960,8 @@ INSERT INTO `sys_transcoder_filters` (`transcoder_object`, `filter`, `filter_par
 ('sys_icon_android_splash', 'Resize', 'a:3:{s:1:"w";s:3:"512";s:1:"h";s:3:"512";s:13:"square_resize";s:1:"1";}', '0'),
 ('sys_icon_facebook', 'Resize', 'a:4:{s:1:"w";s:3:"100";s:1:"h";s:3:"100";s:13:"square_resize";s:1:"1";s:10:"force_type";s:3:"png";}', '0'),
 ('sys_icon_favicon', 'Resize', 'a:4:{s:1:"w";s:2:"16";s:1:"h";s:2:"16";s:13:"square_resize";s:1:"1";s:10:"force_type";s:3:"png";}', '0'),
-('sys_cover', 'Resize', 'a:3:{s:1:"w";s:4:"1920";s:1:"h";s:3:"720";s:10:"force_type";s:3:"png";}', '0'),
-('sys_cover_unit_profile', 'Resize', 'a:3:{s:1:"w";s:3:"640";s:1:"h";s:3:"240";s:10:"force_type";s:3:"png";}', '0'),
+('sys_cover', 'Resize', 'a:3:{s:1:"w";s:4:"1920";s:1:"h";s:3:"720";s:7:"quality";s:2:"82";}', '0'),
+('sys_cover_unit_profile', 'Resize', 'a:3:{s:1:"w";s:3:"640";s:1:"h";s:3:"240";s:7:"quality";s:2:"82";}', '0'),
 ('sys_cover_preview', 'Resize', 'a:3:{s:1:"w";s:3:"120";s:1:"h";s:2:"45";s:10:"force_type";s:3:"jpg";}', '0'),
 ('sys_builder_page_preview', 'Resize', 'a:4:{s:1:"w";s:3:"128";s:1:"h";s:3:"128";s:13:"square_resize";s:1:"1";s:10:"force_type";s:3:"jpg";}', '0'),
 ('sys_builder_page_embed', 'ResizeVar', '', '0'),

+ 2 - 2
modules/boonex/artificer/data/template/system/cover.html

@@ -8,7 +8,7 @@
 <div class="bx-menu-toolbar-padding pt-16">
     <div class="bx-cover-gap overflow-hidden">
         <div class="bx-cover-wrapper<bx_if:empty_cover_class> bx-cover-empty</bx_if:empty_cover_class> relative" <bx_if:bg>style="background-image:url(__image_url__)"</bx_if:bg>>
-            <div class="bx-cover-gradient bx-def-page-width relative mx-auto"></div>
+            <div class="bx-cover-gradient absolute inset-0 pointer-events-none"></div>
             <div class="relative mx-auto py-16 text-center">
                 <bx_if:image>
                     <div class="bx-cover-image">
@@ -17,7 +17,7 @@
                 </bx_if:image>
                 <div class="bx-cover-title flex justify-center items-center">
                     <bx_if:title>
-                    <h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-50">
+                    <h1 class="text-4xl font-bold tracking-tight text-white">
                         <span class="block pb-1 relative">__title__</span>
                         <span class="block pb-1 bg-clip-text text-transparent bg-gradient-to-r from-green-500 to-blue-500"></span>
                     </h1>

+ 4 - 4
modules/boonex/artificer/data/template/system/cover_home.html

@@ -8,14 +8,14 @@
 <div class="bx-menu-toolbar-padding pt-16">
     <div class="bx-cover-gap overflow-hidden">
         <div class="bx-cover-wrapper<bx_if:empty_cover_class> bx-cover-empty</bx_if:empty_cover_class> relative" <bx_if:bg>style="background-image:url(__image_url__)"</bx_if:bg>>
-            <div class="bx-cover-gradient bx-def-page-width relative mx-auto"></div>
+            <div class="bx-cover-gradient absolute inset-0 pointer-events-none"></div>
             <div class="relative mx-auto py-16 text-center">
                 <div>
-                    <h1 class="text-4xl  sm:text-5xl xl:text-6xl tracking-tight  font-bold text-gray-800 dark:text-white ">
+                    <h1 class="text-4xl  sm:text-5xl xl:text-6xl tracking-tight  font-bold text-white">
                         <span class="block pb-1 relative"><bx_text:_bx_artificer_txt_splash_welcome_l1 /></span>
-                        <span class="block pb-1  bg-clip-text text-transparent bg-gradient-to-r from-green-500 to-blue-500"><bx_text:_bx_artificer_txt_splash_welcome_l2 /></span>
+                        <span class="block pb-1  bg-clip-text text-transparent bg-gradient-to-r from-green-200 to-blue-200"><bx_text:_bx_artificer_txt_splash_welcome_l2 /></span>
                     </h1>
-                    <p class="p-8  font-light text-gray-700 dark:text-gray-300 text-base  sm:text-lg xl:text-xl">
+                    <p class="p-8  font-light text-white text-base  sm:text-lg xl:text-xl">
                         <span class="block"><bx_text:_bx_artificer_txt_splash_welcome_l3 /></span>
                     </p>
                     <div class=" w-full flex justify-center items-center">

+ 8 - 0
modules/boonex/artificer/data/template/system/css/cover.css

@@ -14,6 +14,14 @@ import url(../../../../../../../template/css/cover.css);
 .dark .bx-cover-wrapper {
     background-image: url(../images/cover-default-dark.svg);
 }
+/* Scrim under the white cover text. At 55% black even a pure white pixel of an uploaded
+   image keeps the text at 4.5:1; the default art is dark enough without it. */
+.bx-cover-wrapper > .bx-cover-gradient {
+    background-color: rgba(0, 0, 0, 0.55);
+}
+.bx-cover-wrapper.bx-cover-empty > .bx-cover-gradient {
+    background-color: transparent;
+}
 .bx-image-edit-buttons{
     position: absolute;
     left:1rem;

File diff suppressed because it is too large
+ 0 - 0
modules/boonex/artificer/data/template/system/images/cover-default-dark.svg


File diff suppressed because it is too large
+ 0 - 0
modules/boonex/artificer/data/template/system/images/cover-default.svg


+ 4 - 0
modules/boonex/fontawesome/template/css/fonts-all.css

@@ -1,4 +1,5 @@
 @font-face {
+  font-display: swap;
   font-family: 'sys-user';
   font-style: normal;
   font-weight: 900;
@@ -13,6 +14,7 @@
 }
 
 @font-face {
+  font-display: swap;
   font-family: 'sys-user-far';
   font-style: normal;
   font-weight: 400;
@@ -27,6 +29,7 @@
 }
 
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fab';
     font-style: normal;
     font-weight: normal;
@@ -40,6 +43,7 @@
 }
 
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fal';
     font-style: normal;
     font-weight: 300;

+ 2 - 0
modules/boonex/fontawesome/template/css/fonts-duotone.css

@@ -37,6 +37,7 @@
 }
 
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fab';
     font-style: normal;
     font-weight: normal;
@@ -50,6 +51,7 @@
 }
 
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fal';
     font-style: normal;
     font-weight: 300;

+ 2 - 0
modules/boonex/fontawesome/template/css/fonts-light.css

@@ -1,4 +1,5 @@
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fal';
     font-style: normal;
     font-weight: 300;
@@ -15,6 +16,7 @@
 }
 
 @font-face {
+  font-display: swap;
     font-family: 'sys-user-fab';
     font-style: normal;
     font-weight: normal;

+ 9 - 0
scripts/Caddyfile

@@ -9,6 +9,15 @@
 	# Enable compression (optional)
 	encode zstd br gzip
 
+	# Long-lived caching for real static assets. Storage (/s/*) serves files
+	# through storage.php, which sets its own headers (private files included),
+	# so it is excluded here.
+	@static {
+		path *.woff2 *.woff *.ttf *.otf *.eot *.svg *.png *.jpg *.jpeg *.gif *.webp *.avif *.ico *.css *.js *.mjs *.map
+		not path /s/*
+	}
+	header @static Cache-Control "public, max-age=2592000"
+
 	# Add trailing slash for directory requests
 	@canonicalPath {
 		file {path}/index.php

+ 27 - 6
scripts/docker-compose/nginx.conf

@@ -1,26 +1,47 @@
-#
-
 server {
     listen 80 default_server;
     root /opt/una;
-
     client_max_body_size 200M;
 
+    # Compress text responses. HTML is compressed whenever gzip is on; the
+    # types below add CSS/JS/JSON/SVG. Already-compressed responses (the
+    # gzip_loader.php bundles, which set Content-Encoding themselves, and
+    # woff2 fonts) are left untouched by nginx.
+    gzip on;
+    gzip_vary on;
+    gzip_proxied any;
+    gzip_comp_level 5;
+    gzip_min_length 256;
+    gzip_types text/plain text/css text/xml application/javascript text/javascript application/json application/xml image/svg+xml application/manifest+json application/wasm;
+
     location / {
         index index.html index.htm index.php;
-
         rewrite "^/page/(.*)$" /page.php?i=$1 last;
         rewrite "^/m/(.*)$" /modules/index.php?r=$1 last;
         rewrite "^/s/([a-zA-Z0-9_]+)/([a-zA-Z0-9\.]+)" /storage.php?o=$1&f=$2 last;
-
         if (!-e $request_filename) {
             rewrite  ^/(.+)$  /r.php?_q=$1  last;
             break;
         }
     }
 
-    index index.php index.html index.htm;
+    # Long-lived caching for real static files. Virtual paths that merely end
+    # in an asset extension (/s/<obj>/<file>.png served by storage.php, or a
+    # missing file routed through r.php) are not files on disk, so try_files
+    # hands them to @dynamic instead of 404ing.
+    location ~* \.(?:woff2?|ttf|otf|eot|svg|png|jpe?g|gif|webp|avif|ico|css|js|mjs|map)$ {
+        try_files $uri @dynamic;
+        expires 30d;
+        add_header Cache-Control "public";
+        access_log off;
+    }
 
+    location @dynamic {
+        rewrite "^/s/([a-zA-Z0-9_]+)/([a-zA-Z0-9\.]+)" /storage.php?o=$1&f=$2 last;
+        rewrite ^/(.+)$ /r.php?_q=$1 last;
+    }
+
+    index index.php index.html index.htm;
     location ~ \.php$ {
         fastcgi_pass php:9000;
         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

+ 10 - 0
scripts/docker-compose/php-fpm-pool.conf

@@ -0,0 +1,10 @@
+; Loaded after the image's www.conf, which caps the pool at 5 workers.
+; image_transcoder.php keeps its worker while it downloads the original file
+; through the site URL, and that download needs a second worker. With 5 workers,
+; 5 parallel thumbnail requests took them all and hung until nginx returned 504.
+[www]
+pm = dynamic
+pm.max_children = 32
+pm.start_servers = 4
+pm.min_spare_servers = 2
+pm.max_spare_servers = 8

+ 2 - 2
scripts/docker-compose/php.ini

@@ -15,9 +15,9 @@ opcache.enable=1
 opcache.revalidate_freq=0
 opcache.validate_timestamps=1
 opcache.max_accelerated_files=100000
-opcache.memory_consumption=72
+opcache.memory_consumption=256
 opcache.max_wasted_percentage=20
-opcache.interned_strings_buffer=16
+opcache.interned_strings_buffer=32
 opcache.fast_shutdown=1
 
 [xdebug]

+ 1 - 1
scripts/railway/Dockerfile

@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
         libzip-dev \
     && docker-php-ext-configure gd --with-freetype --with-jpeg \
     && docker-php-ext-install -j$(nproc) gd mysqli pdo_mysql zip exif opcache \
-    && a2enmod rewrite \
+    && a2enmod rewrite deflate expires \
     && rm -rf /var/lib/apt/lists/*
 
 RUN printf '%s\n' \

+ 3 - 0
template/css/icons.css

@@ -4,6 +4,7 @@
  */
 
 @font-face {
+  font-display: swap;
   font-family: 'sys-user';
   font-style: normal;
   font-weight: 900;
@@ -18,6 +19,7 @@
 }
 
 @font-face {
+  font-display: swap;
   font-family: 'sys-user-far';
   font-style: normal;
   font-weight: 400;
@@ -31,6 +33,7 @@
 }
 
 @font-face {
+  font-display: swap;
   font-family: 'sys-user-fab';
   font-style: normal;
   font-weight: normal;

+ 6 - 0
tests/bootstrap.php

@@ -19,6 +19,12 @@ if (!file_exists($sHeaderPath))
 
 require_once($sHeaderPath);
 
+// The global _t() is declared at the bottom of BxDolLanguages.php, so it only exists once that class
+// file has been loaded. A web request loads it while bootstrapping; the installer-generated header
+// used by CI does not, and unit tests that compare against _t() strings failed there with
+// "Call to undefined function _t()". Load it up front, as api.php does.
+bx_import('BxDolLanguages');
+
 class BxDolTestCase extends \PHPUnit\Framework\TestCase
 {
     /**

Some files were not shown because too many files changed in this diff