From 42fd0a78e1a9ad5006da0e413a70722ad8420bb3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 14:02:35 -0500 Subject: [PATCH 01/41] =?UTF-8?q?Restore=20updates.xml=20=E2=80=94=20bulk?= =?UTF-8?q?=20sync=20overwrote=20it=20again=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- updates.xml | 151 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 37 deletions(-) diff --git a/updates.xml b/updates.xml index f69d346..3230e5a 100644 --- a/updates.xml +++ b/updates.xml @@ -1,39 +1,116 @@ - - This file is the update server manifest for {{EXTENSION_NAME}}. - The Joomla installer polls this URL to check for new versions. - - The manifest.xml in this repository must reference this file: - - - https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/updates.xml - - - https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/updates.xml - - - - When a new release is made, run `make release` or the release workflow to - prepend a new entry to this file automatically. ---> - - {{EXTENSION_NAME}} - MokoCassiopeia — Moko Consulting Joomla extension - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{VERSION}} - - - https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip - - - - 8.1 - - \ No newline at end of file + + + + MokoCassiopeia + MokoCassiopeia development build — unstable. + mokocassiopeia + template + site + 03.10.10 + 2026-04-19 + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development + + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + + fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + development + Moko Consulting + https://mokoconsulting.tech + + 8.1 + + + + + MokoCassiopeia + MokoCassiopeia alpha build — early testing. + mokocassiopeia + template + site + 03.10.10 + 2026-04-19 + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha + + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + + fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + alpha + Moko Consulting + https://mokoconsulting.tech + + 8.1 + + + + + MokoCassiopeia + MokoCassiopeia beta build — feature complete, stability testing. + mokocassiopeia + template + site + 03.10.10 + 2026-04-19 + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta + + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + + fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + beta + Moko Consulting + https://mokoconsulting.tech + + 8.1 + + + + + MokoCassiopeia + MokoCassiopeia release candidate — testing only. + mokocassiopeia + template + site + 03.10.10 + 2026-04-19 + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate + + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + + fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + rc + Moko Consulting + https://mokoconsulting.tech + + 8.1 + + + + + MokoCassiopeia + Moko Consulting's site template based on Cassiopeia. + mokocassiopeia + template + site + 03.10.10 + 2026-04-19 + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 + + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + + fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + stable + Moko Consulting + https://mokoconsulting.tech + + 8.1 + + + From 7e31ea028ffbd7f7df9149096808b4f6ba7917f3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 17:35:28 -0500 Subject: [PATCH 02/41] =?UTF-8?q?Bump=20version=2003.10.10=20=E2=86=92=200?= =?UTF-8?q?3.10.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/joomla.asset.json | 2 +- src/templateDetails.xml | 4 ++-- updates.xml | 26 +++++++++++++------------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a3a0042..15f97be 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.10 + VERSION: 03.10.11 BRIEF: Documentation for MokoCassiopeia template --> # MokoCassiopeia → MokoOnyx -> **This template is being renamed to MokoOnyx.** Version 03.10.10 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. +> **This template is being renamed to MokoOnyx.** Version 03.10.11 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. **A Modern, Lightweight Joomla Template Based on Cassiopeia** diff --git a/src/joomla.asset.json b/src/joomla.asset.json index 915411b..d7e96fd 100644 --- a/src/joomla.asset.json +++ b/src/joomla.asset.json @@ -17,7 +17,7 @@ "defgroup": "Joomla.Template.Site", "ingroup": "MokoCassiopeia.Template.Assets", "path": "./media/templates/site/mokocassiopeia/joomla.asset.json", - "version": "03.10.10", + "version": "03.10.11", "brief": "Joomla asset registry for MokoCassiopeia" } }, diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 13a6271..cb7e4a9 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,13 +39,13 @@ MokoCassiopeia - 03.10.10 + 03.10.11 script.php 2026-04-15 Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting - Version 03.10.10 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
+ Version 03.10.11 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
1 component.php diff --git a/updates.xml b/updates.xml index 3230e5a..5c61966 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.10 + 03.10.11 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d development @@ -34,11 +34,11 @@ mokocassiopeia template site - 03.10.10 + 03.10.11 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d alpha @@ -55,11 +55,11 @@ mokocassiopeia template site - 03.10.10 + 03.10.11 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d beta @@ -76,12 +76,12 @@ mokocassiopeia template site - 03.10.10 + 03.10.11 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d rc @@ -98,12 +98,12 @@ mokocassiopeia template site - 03.10.10 + 03.10.11 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.10.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d stable From fd012b4541d09355fbccd6f3a8dd134d1d5d9de0 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 19 Apr 2026 22:35:52 +0000 Subject: [PATCH 03/41] chore: update development SHA-256 for 03.10.11 [skip ci] --- updates.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/updates.xml b/updates.xml index 5c61966..5c93b71 100644 --- a/updates.xml +++ b/updates.xml @@ -17,9 +17,9 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.11-dev.zip - fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + 2a27ca2e6c6b6a570a437f4d1b3e40bb54cdc37e3998fed10970a7f50593c8ad development Moko Consulting https://mokoconsulting.tech From 9ce333ebe11cdab0ef74468ef5e2c26e64272bf1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 17:38:02 -0500 Subject: [PATCH 04/41] Bridge: download and install MokoOnyx from Gitea release Instead of copying/renaming files, the bridge now: 1. Downloads mokoonyx ZIP from Gitea releases 2. Installs via Joomla's Installer (proper extension registration) 3. Migrates template styles and default assignment 4. Falls back gracefully with manual install link if download fails Co-Authored-By: Claude Opus 4.6 (1M context) --- src/helper/bridge.php | 395 +++++++++++++++++------------------------- 1 file changed, 161 insertions(+), 234 deletions(-) diff --git a/src/helper/bridge.php b/src/helper/bridge.php index 71d2546..998e036 100644 --- a/src/helper/bridge.php +++ b/src/helper/bridge.php @@ -10,16 +10,14 @@ /** * Bridge migration helper — MokoCassiopeia → MokoOnyx * - * Called from script.php during the v03.10.00 update. Copies the template - * to the new directory name, migrates database records, and sets MokoOnyx - * as the active site template. + * Downloads and installs MokoOnyx from the Gitea release, then migrates + * template styles and menu assignments from MokoCassiopeia. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\CMS\Filesystem\File; -use Joomla\CMS\Filesystem\Folder; +use Joomla\CMS\Installer\Installer; use Joomla\CMS\Log\Log; class MokoBridgeMigration @@ -30,179 +28,166 @@ class MokoBridgeMigration private const OLD_DISPLAY = 'MokoCassiopeia'; private const NEW_DISPLAY = 'MokoOnyx'; + /** URL to the latest MokoOnyx stable release ZIP */ + private const RELEASE_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip'; + /** * Run the full migration. - * - * @return bool True on success, false on failure. */ public static function run(): bool { $app = Factory::getApplication(); - $db = Factory::getDbo(); - // 1. Copy template files - if (!self::copyTemplateFiles()) { + // Check if MokoOnyx is already installed + if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { + self::log('MokoOnyx already installed — skipping download.'); + self::migrateStyles(); + self::notifyUser($app); + return true; + } + + // 1. Download MokoOnyx ZIP + $zipPath = self::downloadRelease(); + if (!$zipPath) { $app->enqueueMessage( - 'MokoOnyx migration: failed to copy template files. ' - . 'You can manually copy templates/mokocassiopeia to templates/mokoonyx.', - 'error' + 'MokoOnyx migration: could not download the MokoOnyx template package. ' + . 'Please install MokoOnyx manually from ' + . 'Gitea Releases.', + 'warning' ); return false; } - // 2. Copy media files - if (!self::copyMediaFiles()) { + // 2. Install MokoOnyx via Joomla's installer + $installed = self::installPackage($zipPath); + + // Clean up downloaded ZIP + @unlink($zipPath); + + if (!$installed) { $app->enqueueMessage( - 'MokoOnyx migration: failed to copy media files. ' - . 'You can manually copy media/templates/site/mokocassiopeia to media/templates/site/mokoonyx.', + 'MokoOnyx migration: installation failed. ' + . 'Please install MokoOnyx manually from ' + . 'Gitea Releases.', 'warning' ); + return false; } - // 3. Rename internals in the new copy (templateDetails.xml, language files, etc.) - self::renameInternals(); + // 3. Migrate template styles + self::migrateStyles(); - // 4. Register the new template in the database - self::migrateDatabase($db); - - // 5. Notify the admin - $app->enqueueMessage( - 'MokoCassiopeia has been renamed to MokoOnyx.
' - . 'Your template settings have been migrated automatically. ' - . 'MokoOnyx is now your active site template. ' - . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', - 'success' - ); + // 4. Notify admin + self::notifyUser($app); self::log('Bridge migration completed successfully.'); return true; } /** - * Copy template directory. + * Download the MokoOnyx ZIP to Joomla's tmp directory. */ - private static function copyTemplateFiles(): bool + private static function downloadRelease(): ?string { - $src = JPATH_ROOT . '/templates/' . self::OLD_NAME; - $dst = JPATH_ROOT . '/templates/' . self::NEW_NAME; + $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); + $zipPath = $tmpDir . '/mokoonyx-install.zip'; - if (is_dir($dst)) { - self::log('MokoOnyx template directory already exists — skipping copy.'); - return true; + $content = false; + + // Method 1: file_get_contents + if (ini_get('allow_url_fopen')) { + $ctx = stream_context_create([ + 'http' => [ + 'timeout' => 60, + 'follow_location' => true, + 'max_redirects' => 5, + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + ]); + $content = @file_get_contents(self::RELEASE_URL, false, $ctx); } - if (!is_dir($src)) { - self::log('Source template directory not found: ' . $src, 'error'); - return false; - } + // Method 2: cURL + if ($content === false && function_exists('curl_init')) { + $ch = curl_init(self::RELEASE_URL); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 60, + CURLOPT_SSL_VERIFYPEER => true, + ]); + $content = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - return Folder::copy($src, $dst); - } - - /** - * Copy media directory. - */ - private static function copyMediaFiles(): bool - { - $src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; - $dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; - - if (is_dir($dst)) { - self::log('MokoOnyx media directory already exists — skipping copy.'); - return true; - } - - if (!is_dir($src)) { - self::log('Source media directory not found: ' . $src, 'warning'); - return true; // Non-critical - } - - return Folder::copy($src, $dst); - } - - /** - * Rename internal references in the copied template. - */ - private static function renameInternals(): void - { - $base = JPATH_ROOT . '/templates/' . self::NEW_NAME; - $mediaBase = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; - - // templateDetails.xml — name, element, update servers, paths - $manifest = $base . '/templateDetails.xml'; - if (is_file($manifest)) { - $content = file_get_contents($manifest); - $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); - $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); - // Update the update server URLs to point to MokoOnyx repo - $content = str_replace('MokoCassiopeia', 'MokoOnyx', $content); - file_put_contents($manifest, $content); - self::log('Updated templateDetails.xml for MokoOnyx.'); - } - - // joomla.asset.json - $assetFile = $base . '/joomla.asset.json'; - if (is_file($assetFile)) { - $content = file_get_contents($assetFile); - $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); - $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); - file_put_contents($assetFile, $content); - } - - // Language files - $langDirs = [ - $base . '/language/en-GB', - $base . '/language/en-US', - ]; - foreach ($langDirs as $langDir) { - if (!is_dir($langDir)) continue; - - foreach (glob($langDir . '/*mokocassiopeia*') as $file) { - $newFile = str_replace(self::OLD_NAME, self::NEW_NAME, $file); - if (is_file($file)) { - $content = file_get_contents($file); - $content = str_replace('MOKOCASSIOPEIA', 'MOKOONYX', $content); - $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); - $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); - file_put_contents($newFile, $content); - if ($newFile !== $file) { - File::delete($file); - } - } + if ($httpCode !== 200) { + $content = false; } } - // script.php — class name - $scriptFile = $base . '/script.php'; - if (is_file($scriptFile)) { - $content = file_get_contents($scriptFile); - $content = str_replace('Tpl_MokocassiopeiaInstallerScript', 'Tpl_MokoonyxInstallerScript', $content); - $content = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $content); - $content = str_replace(self::OLD_NAME, self::NEW_NAME, $content); - // Remove the bridge migration call from the new template's script - $content = preg_replace( - '/\/\/ Bridge migration.*?MokoBridgeMigration::run\(\);/s', - '// Migration complete — this is MokoOnyx', - $content - ); - file_put_contents($scriptFile, $content); + if ($content === false || strlen($content) < 1000) { + self::log('Bridge: failed to download MokoOnyx ZIP from ' . self::RELEASE_URL, 'error'); + return null; } - // Remove bridge helper from the new template (not needed) - $bridgeFile = $base . '/helper/bridge.php'; - if (is_file($bridgeFile)) { - File::delete($bridgeFile); + if (file_put_contents($zipPath, $content) === false) { + self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error'); + return null; } - self::log('Renamed internal references in MokoOnyx.'); + self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)'); + return $zipPath; } /** - * Migrate database records: template_styles, menu assignments. + * Install the downloaded ZIP via Joomla's Installer. */ - private static function migrateDatabase(\Joomla\Database\DatabaseInterface $db): void + private static function installPackage(string $zipPath): bool { - // Get existing MokoCassiopeia styles + try { + $installer = Installer::getInstance(); + + $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); + $extractDir = $tmpDir . '/mokoonyx_install_' . time(); + + $zip = new \ZipArchive(); + if ($zip->open($zipPath) !== true) { + self::log('Bridge: failed to open ZIP', 'error'); + return false; + } + $zip->extractTo($extractDir); + $zip->close(); + + $result = $installer->install($extractDir); + + if (is_dir($extractDir)) { + self::removeDirectory($extractDir); + } + + if ($result) { + self::log('Bridge: MokoOnyx installed via Joomla Installer'); + } else { + self::log('Bridge: Joomla Installer returned false', 'error'); + } + + return (bool) $result; + } catch (\Throwable $e) { + self::log('Bridge: install failed: ' . $e->getMessage(), 'error'); + return false; + } + } + + /** + * Migrate template styles and menu assignments from MokoCassiopeia to MokoOnyx. + */ + private static function migrateStyles(): void + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) ->select('*') ->from('#__template_styles') @@ -211,136 +196,78 @@ class MokoBridgeMigration $oldStyles = $db->setQuery($query)->loadObjectList(); if (empty($oldStyles)) { - self::log('No MokoCassiopeia styles found in database.', 'warning'); + self::log('No MokoCassiopeia styles found — nothing to migrate.'); return; } foreach ($oldStyles as $oldStyle) { - // Check if MokoOnyx style already exists + $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); $query = $db->getQuery(true) ->select('COUNT(*)') ->from('#__template_styles') ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('title') . ' = ' . $db->quote( - str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title) - )); - $exists = (int) $db->setQuery($query)->loadResult(); - - if ($exists > 0) { + ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle)); + if ((int) $db->setQuery($query)->loadResult() > 0) { continue; } - // Create new style with same params $newStyle = clone $oldStyle; unset($newStyle->id); $newStyle->template = self::NEW_NAME; - $newStyle->title = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); + $newStyle->title = $newTitle; - // Update params: replace any mokocassiopeia paths - $params = $newStyle->params; - if (is_string($params)) { - $params = str_replace(self::OLD_NAME, self::NEW_NAME, $params); - $newStyle->params = $params; + if (is_string($newStyle->params)) { + $newStyle->params = str_replace(self::OLD_NAME, self::NEW_NAME, $newStyle->params); } $db->insertObject('#__template_styles', $newStyle, 'id'); $newId = $newStyle->id; - // Copy menu assignments - $query = $db->getQuery(true) - ->select('menuid') - ->from('#__template_styles_menus') // Joomla 5 uses this table - ->where('template_style_id = ' . (int) $oldStyle->id); - - try { - $menuIds = $db->setQuery($query)->loadColumn(); - foreach ($menuIds as $menuId) { - $obj = (object) [ - 'template_style_id' => $newId, - 'menuid' => $menuId, - ]; - $db->insertObject('#__template_styles_menus', $obj); - } - } catch (\Exception $e) { - // Table may not exist in all Joomla versions - } - - // If this was the default style, make MokoOnyx the default if ($oldStyle->home == 1) { - // Set MokoOnyx as default - $query = $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 1') - ->where('id = ' . (int) $newId); - $db->setQuery($query)->execute(); + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 1') + ->where('id = ' . (int) $newId) + )->execute(); - // Unset MokoCassiopeia as default - $query = $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 0') - ->where('id = ' . (int) $oldStyle->id); - $db->setQuery($query)->execute(); + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 0') + ->where('id = ' . (int) $oldStyle->id) + )->execute(); self::log('Set MokoOnyx as default site template.'); } } - // Register the new template in the extensions table - self::registerExtension($db); - - self::log('Database migration completed. ' . count($oldStyles) . ' style(s) migrated.'); + self::log('Migrated ' . count($oldStyles) . ' template style(s).'); } - /** - * Register MokoOnyx in the extensions table so Joomla recognizes it. - */ - private static function registerExtension(\Joomla\Database\DatabaseInterface $db): void + private static function notifyUser($app): void { - // Check if already registered - $query = $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - $existing = $db->setQuery($query)->loadResult(); - - if ($existing) { - self::log('MokoOnyx already registered in extensions table.'); - return; - } - - // Get the old extension record as a base - $query = $db->getQuery(true) - ->select('*') - ->from('#__extensions') - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - $oldExt = $db->setQuery($query)->loadObject(); - - if (!$oldExt) { - self::log('MokoCassiopeia extension record not found.', 'warning'); - return; - } - - $newExt = clone $oldExt; - unset($newExt->extension_id); - $newExt->element = self::NEW_NAME; - $newExt->name = self::NEW_NAME; - - // Update manifest_cache with new name - $cache = json_decode($newExt->manifest_cache, true); - if (is_array($cache)) { - $cache['name'] = self::NEW_DISPLAY; - $newExt->manifest_cache = json_encode($cache); - } - - $db->insertObject('#__extensions', $newExt, 'extension_id'); - self::log('Registered MokoOnyx in extensions table (ID: ' . $newExt->extension_id . ').'); + $app->enqueueMessage( + 'MokoCassiopeia has been renamed to MokoOnyx.
' + . 'Your template settings have been migrated automatically. ' + . 'MokoOnyx is now your active site template. ' + . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', + 'success' + ); + } + + private static function removeDirectory(string $dir): void + { + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($items as $item) { + $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); + } + rmdir($dir); } - /** - * Log a message. - */ private static function log(string $message, string $priority = 'info'): void { $priorities = [ From da567cecd4be32f24773bfd2a0f36c2e7a4fb5c8 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 19 Apr 2026 22:43:44 +0000 Subject: [PATCH 05/41] chore: update development SHA-256 for 03.10.11 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 5c93b71..38420c3 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.11-dev.zip - 2a27ca2e6c6b6a570a437f4d1b3e40bb54cdc37e3998fed10970a7f50593c8ad + 1e34ddbe6a28114100fcb1bb995b9a7b4ccbec8468a7a1f73022440f4ac3ddfc development Moko Consulting https://mokoconsulting.tech From 4e6f14c3baac604a4a943c6cc94f110d03caf635 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 17:51:02 -0500 Subject: [PATCH 06/41] Bridge: install from release, copy user files + params after. Bump 03.10.12 1. Download & install MokoOnyx from Gitea release 2. Copy user files (custom themes, user.css/js) to MokoOnyx 3. Migrate template styles with params Fallback: if download fails, copy user files only (MokoOnyx must be installed manually) Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 +- src/helper/bridge.php | 85 ++++++++++++++++++++++++++++++++--------- src/joomla.asset.json | 2 +- src/templateDetails.xml | 6 +-- updates.xml | 26 ++++++------- 5 files changed, 87 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 15f97be..9c845f5 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.11 + VERSION: 03.10.12 BRIEF: Documentation for MokoCassiopeia template --> # MokoCassiopeia → MokoOnyx -> **This template is being renamed to MokoOnyx.** Version 03.10.11 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. +> **This template is being renamed to MokoOnyx.** Version 03.10.12 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. **A Modern, Lightweight Joomla Template Based on Cassiopeia** diff --git a/src/helper/bridge.php b/src/helper/bridge.php index 998e036..43a168a 100644 --- a/src/helper/bridge.php +++ b/src/helper/bridge.php @@ -46,27 +46,23 @@ class MokoBridgeMigration return true; } - // 1. Download MokoOnyx ZIP + // 1. Try downloading and installing MokoOnyx from Gitea release + $installed = false; $zipPath = self::downloadRelease(); - if (!$zipPath) { - $app->enqueueMessage( - 'MokoOnyx migration: could not download the MokoOnyx template package. ' - . 'Please install MokoOnyx manually from ' - . 'Gitea Releases.', - 'warning' - ); - return false; + if ($zipPath) { + $installed = self::installPackage($zipPath); + @unlink($zipPath); } - // 2. Install MokoOnyx via Joomla's installer - $installed = self::installPackage($zipPath); - - // Clean up downloaded ZIP - @unlink($zipPath); + // 2. Fallback: copy from MokoCassiopeia and rename + if (!$installed) { + self::log('Bridge: download/install failed, falling back to file copy'); + $installed = self::copyAndRename(); + } if (!$installed) { $app->enqueueMessage( - 'MokoOnyx migration: installation failed. ' + 'MokoOnyx migration: automatic installation failed. ' . 'Please install MokoOnyx manually from ' . 'Gitea Releases.', 'warning' @@ -74,10 +70,13 @@ class MokoBridgeMigration return false; } - // 3. Migrate template styles + // 3. Copy user files (custom themes, user.css, user.js) + self::copyAndRename(); + + // 4. Migrate template styles and params self::migrateStyles(); - // 4. Notify admin + // 5. Notify admin self::notifyUser($app); self::log('Bridge migration completed successfully.'); @@ -245,6 +244,58 @@ class MokoBridgeMigration self::log('Migrated ' . count($oldStyles) . ' template style(s).'); } + /** + * Copy user-specific files from MokoCassiopeia to MokoOnyx. + * Only copies custom themes, user.css, and user.js — not the full template. + * MokoOnyx must already be installed (via download or manual). + */ + private static function copyAndRename(): bool + { + $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; + $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + + if (!is_dir($newMedia)) { + self::log('Bridge: MokoOnyx media dir not found — cannot copy user files', 'warning'); + return false; + } + + $copied = 0; + + // Copy custom theme palettes + $userFiles = [ + 'css/theme/light.custom.css', + 'css/theme/dark.custom.css', + 'css/theme/light.custom.min.css', + 'css/theme/dark.custom.min.css', + 'css/user.css', + 'css/user.min.css', + 'js/user.js', + 'js/user.min.js', + ]; + + foreach ($userFiles as $relPath) { + $srcFile = $oldMedia . '/' . $relPath; + $dstFile = $newMedia . '/' . $relPath; + if (is_file($srcFile) && !is_file($dstFile)) { + $dstDir = dirname($dstFile); + if (!is_dir($dstDir)) { + mkdir($dstDir, 0755, true); + } + copy($srcFile, $dstFile); + $copied++; + } + } + + // Copy favicon directory + $faviconSrc = JPATH_ROOT . '/images/favicons'; + if (is_dir($faviconSrc)) { + self::log('Bridge: favicons already at images/favicons — shared between templates'); + } + + self::log("Bridge: copied {$copied} user file(s) to MokoOnyx"); + return true; + } + private static function notifyUser($app): void { $app->enqueueMessage( diff --git a/src/joomla.asset.json b/src/joomla.asset.json index d7e96fd..fbe1607 100644 --- a/src/joomla.asset.json +++ b/src/joomla.asset.json @@ -17,7 +17,7 @@ "defgroup": "Joomla.Template.Site", "ingroup": "MokoCassiopeia.Template.Assets", "path": "./media/templates/site/mokocassiopeia/joomla.asset.json", - "version": "03.10.11", + "version": "03.10.12", "brief": "Joomla asset registry for MokoCassiopeia" } }, diff --git a/src/templateDetails.xml b/src/templateDetails.xml index cb7e4a9..4fef7f3 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,13 +39,13 @@ MokoCassiopeia - 03.10.11 + 03.10.12 script.php - 2026-04-15 + 2026-04-19 Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting - Version 03.10.11 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
+ Version 03.10.12 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
1 component.php diff --git a/updates.xml b/updates.xml index 38420c3..ef50d31 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.11 + 03.10.12 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.11-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.12-dev.zip 1e34ddbe6a28114100fcb1bb995b9a7b4ccbec8468a7a1f73022440f4ac3ddfc development @@ -34,11 +34,11 @@ mokocassiopeia template site - 03.10.11 + 03.10.12 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d alpha @@ -55,11 +55,11 @@ mokocassiopeia template site - 03.10.11 + 03.10.12 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d beta @@ -76,12 +76,12 @@ mokocassiopeia template site - 03.10.11 + 03.10.12 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d rc @@ -98,12 +98,12 @@ mokocassiopeia template site - 03.10.11 + 03.10.12 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.11.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d stable From 85f98042d9c14d393929fa9115dfed669a5135c4 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 19 Apr 2026 22:51:47 +0000 Subject: [PATCH 07/41] chore: update development SHA-256 for 03.10.12 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index ef50d31..4726168 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.12-dev.zip - 1e34ddbe6a28114100fcb1bb995b9a7b4ccbec8468a7a1f73022440f4ac3ddfc + 734be87ddcd206534173347666fd6ed3feed301b82fbd1a5d95d58d4bf9865c8 development Moko Consulting https://mokoconsulting.tech From 0d14a05c61f16de50cc8c42d1dffccf04687e1b4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 18:28:57 -0500 Subject: [PATCH 08/41] Move bridge to postflight() + add logging to update(). Bump 03.10.13 Joomla may not call update() for template updates. postflight() is more reliably triggered. Also added version logging to update() to diagnose if it's being called at all. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- src/joomla.asset.json | 2 +- src/script.php | 17 ++++++++++++++++- src/templateDetails.xml | 4 ++-- updates.xml | 26 +++++++++++++------------- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9c845f5..58c0deb 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.12 + VERSION: 03.10.13 BRIEF: Documentation for MokoCassiopeia template --> # MokoCassiopeia → MokoOnyx -> **This template is being renamed to MokoOnyx.** Version 03.10.12 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. +> **This template is being renamed to MokoOnyx.** Version 03.10.13 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. **A Modern, Lightweight Joomla Template Based on Cassiopeia** diff --git a/src/joomla.asset.json b/src/joomla.asset.json index fbe1607..16ca025 100644 --- a/src/joomla.asset.json +++ b/src/joomla.asset.json @@ -17,7 +17,7 @@ "defgroup": "Joomla.Template.Site", "ingroup": "MokoCassiopeia.Template.Assets", "path": "./media/templates/site/mokocassiopeia/joomla.asset.json", - "version": "03.10.12", + "version": "03.10.13", "brief": "Joomla asset registry for MokoCassiopeia" } }, diff --git a/src/script.php b/src/script.php index 793548a..deec459 100644 --- a/src/script.php +++ b/src/script.php @@ -97,7 +97,7 @@ class Tpl_MokocassiopeiaInstallerScript */ public function update(InstallerAdapter $parent): bool { - $this->logMessage('MokoCassiopeia template updated to v03.10.00 (bridge release).'); + $this->logMessage('MokoCassiopeia update() called — version ' . ($parent->getManifest()->version ?? 'unknown')); // Run CSS variable sync to inject any new variables into user's custom palettes. $synced = $this->syncCustomVariables($parent); @@ -148,6 +148,21 @@ class Tpl_MokocassiopeiaInstallerScript */ public function postflight(string $type, InstallerAdapter $parent): bool { + // Bridge migration runs in postflight (more reliable than update() for templates) + if ($type === 'update') { + $bridgeScript = $parent->getParent()->getPath('source') . '/helper/bridge.php'; + if (!is_file($bridgeScript)) { + $bridgeScript = __DIR__ . '/helper/bridge.php'; + } + if (is_file($bridgeScript)) { + require_once $bridgeScript; + if (class_exists('MokoBridgeMigration')) { + $this->logMessage('Running MokoOnyx bridge migration from postflight...'); + MokoBridgeMigration::run(); + } + } + } + return true; } diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 4fef7f3..66e37ee 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,13 +39,13 @@ MokoCassiopeia - 03.10.12 + 03.10.13 script.php 2026-04-19 Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting - Version 03.10.12 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
+ Version 03.10.13 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]>
1 component.php diff --git a/updates.xml b/updates.xml index 4726168..a49467c 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.12 + 03.10.13 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.12-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.13-dev.zip 734be87ddcd206534173347666fd6ed3feed301b82fbd1a5d95d58d4bf9865c8 development @@ -34,11 +34,11 @@ mokocassiopeia template site - 03.10.12 + 03.10.13 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d alpha @@ -55,11 +55,11 @@ mokocassiopeia template site - 03.10.12 + 03.10.13 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d beta @@ -76,12 +76,12 @@ mokocassiopeia template site - 03.10.12 + 03.10.13 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d rc @@ -98,12 +98,12 @@ mokocassiopeia template site - 03.10.12 + 03.10.13 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.12.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d stable From 12b32b98e2909bff0350794cec513523ebd19228 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 19 Apr 2026 23:32:14 +0000 Subject: [PATCH 09/41] chore: update development SHA-256 for 03.10.13 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index a49467c..93eb1d1 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.13-dev.zip - 734be87ddcd206534173347666fd6ed3feed301b82fbd1a5d95d58d4bf9865c8 + f81685110d6a89558aea7d34255601441ade2d480f191e98baff5db3b40f302a development Moko Consulting https://mokoconsulting.tech From 69a0ca6eafa029c78f0e2590f6b6a46fa490e32f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 11:52:22 -0500 Subject: [PATCH 10/41] Bump 03.10.14 (dev channel only) Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- src/media/css/template.css | 16 ++++++++-------- src/media/images/teaser_bg_sm.png | Bin 979 -> 0 bytes src/media/images/template_preview.png | Bin 63908 -> 0 bytes src/media/images/template_thumbnail.png | Bin 10747 -> 39478 bytes src/templateDetails.xml | 2 +- updates.xml | 8 ++++---- 7 files changed, 15 insertions(+), 15 deletions(-) delete mode 100644 src/media/images/teaser_bg_sm.png delete mode 100644 src/media/images/template_preview.png diff --git a/README.md b/README.md index 58c0deb..a309ee3 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.13 + VERSION: 03.10.14 BRIEF: Documentation for MokoCassiopeia template --> # MokoCassiopeia → MokoOnyx -> **This template is being renamed to MokoOnyx.** Version 03.10.13 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. +> **This template is being renamed to MokoOnyx.** Version 03.10.14 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. **A Modern, Lightweight Joomla Template Based on Cassiopeia** diff --git a/src/media/css/template.css b/src/media/css/template.css index b89fdfe..354e933 100644 --- a/src/media/css/template.css +++ b/src/media/css/template.css @@ -2593,8 +2593,8 @@ progress { font-size: 1rem; font-weight: 400; line-height: 1; - color: var(--input-color, #e6ebf1); - background-color: var(--input-bg, #1a2332); + color: var(--input-color, #1a2332); + background-color: var(--input-bg, #e6ebf1); background-clip: padding-box; border: 1px solid var(--input-border-color, #3a4250); -webkit-appearance: none; @@ -13912,7 +13912,7 @@ meter { height: 4px; margin: 1rem auto 2rem; content: ""; - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); } .container-banner .banner-overlay .overlay .text-thin .lead { @@ -14099,7 +14099,7 @@ td .form-control { margin: 0.5em; color: hsl(0, 0%, 0%); text-align: start; - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); border: 1px solid hsl(210, 7%, 46%); border-radius: 0.25rem; -webkit-box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.8); @@ -15549,7 +15549,7 @@ joomla-alert { min-height: 43px; padding: 0.25rem; color: var(--subhead-color, #9fa6ad); - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); -webkit-box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027); box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027); } @@ -15591,7 +15591,7 @@ joomla-alert { font-size: 1rem; line-height: 2.45rem; color: var(--subhead-color, #9fa6ad); - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); border-color: hsl(210, 11%, 71%); } @@ -16330,7 +16330,7 @@ body:not(.has-sidebar-right) .site-grid .container-component { .nav-tabs+.tab-content { padding: 0.9375rem; - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); border: 1px solid; border-color: hsl(210, 14%, 89%); border-radius: 0 0 0.25rem 0.25rem; @@ -16405,7 +16405,7 @@ body:not(.has-sidebar-right) .site-grid .container-component { } .chosen-container.chosen-container-single .chosen-drop { - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); border: 1px solid hsl(210, 14%, 83%); } diff --git a/src/media/images/teaser_bg_sm.png b/src/media/images/teaser_bg_sm.png deleted file mode 100644 index 94d8fbe63ec636cb2426fc1d01375bc471eca668..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 979 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

#FPJ%`K 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 ZzX5=OCe3xF!c+hN002ovPDHLkV1iG=Tu=Z2 diff --git a/src/media/images/template_thumbnail.png b/src/media/images/template_thumbnail.png index e237919ecf79dbf84ba1f87e908928757635fd92..1a05a0e5fc9e8eb58129f859412491848c540bc5 100644 GIT binary patch literal 39478 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 literal 10747 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 MokoCassiopeia - 03.10.13 + 03.10.14 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 93eb1d1..caf43a3 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.13 - 2026-04-19 + 03.10.14 + 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.13-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.14-dev.zip f81685110d6a89558aea7d34255601441ade2d480f191e98baff5db3b40f302a development From 532ead999d4c9dac0cb88041ad1bc8d4b8edfc20 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 16:53:26 +0000 Subject: [PATCH 11/41] chore: update development SHA-256 for 03.10.14 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index caf43a3..f7ebd47 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.14-dev.zip - f81685110d6a89558aea7d34255601441ade2d480f191e98baff5db3b40f302a + 62b91d1e23146d038967145d89a7895b3bdc38719ff9d0009d89af7bbc9a211c development Moko Consulting https://mokoconsulting.tech From 4d20eee830025cb65efa883f9a543085c42ae1ae Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:01:51 -0500 Subject: [PATCH 12/41] Bridge: fetch stable URL from MokoOnyx updates.xml, fix double-exec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove hardcoded RELEASE_URL, discover from updates.xml stable channel - Add discoverStableUrl() to parse MokoOnyx updates.xml at runtime - Extract httpGet() helper for reuse across download + XML fetch - Remove bridge call from update() — postflight() handles it - Always targets stable channel for production-safe installs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/helper/bridge.php | 106 +++++++++++++++++++++++++++++++++++------- src/script.php | 10 +--- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/helper/bridge.php b/src/helper/bridge.php index 43a168a..9172027 100644 --- a/src/helper/bridge.php +++ b/src/helper/bridge.php @@ -28,8 +28,11 @@ class MokoBridgeMigration private const OLD_DISPLAY = 'MokoCassiopeia'; private const NEW_DISPLAY = 'MokoOnyx'; - /** URL to the latest MokoOnyx stable release ZIP */ - private const RELEASE_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip'; + /** Raw URL for MokoOnyx updates.xml on main — used to discover the stable download URL */ + private const UPDATES_XML_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; + + /** Fallback URL if updates.xml cannot be parsed */ + private const FALLBACK_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip'; /** * Run the full migration. @@ -85,15 +88,94 @@ class MokoBridgeMigration /** * Download the MokoOnyx ZIP to Joomla's tmp directory. + * + * Reads MokoOnyx's updates.xml on main to discover the current stable + * download URL, falling back to a hardcoded URL if parsing fails. */ private static function downloadRelease(): ?string { $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); $zipPath = $tmpDir . '/mokoonyx-install.zip'; + // 1. Discover the stable download URL from MokoOnyx's updates.xml + $releaseUrl = self::discoverStableUrl(); + if (!$releaseUrl) { + self::log('Bridge: could not discover release URL from updates.xml, using fallback'); + $releaseUrl = self::FALLBACK_URL; + } + + self::log('Bridge: downloading MokoOnyx from ' . $releaseUrl); + + // 2. Download the ZIP + $content = self::httpGet($releaseUrl); + + if ($content === false || strlen($content) < 1000) { + self::log('Bridge: failed to download MokoOnyx ZIP from ' . $releaseUrl, 'error'); + return null; + } + + if (file_put_contents($zipPath, $content) === false) { + self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error'); + return null; + } + + self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)'); + return $zipPath; + } + + /** + * Fetch MokoOnyx's updates.xml and extract the stable channel ZIP URL. + * + * Always targets the stable channel — the bridge should only install + * production-ready builds of MokoOnyx. + */ + private static function discoverStableUrl(): ?string + { + $xml = self::httpGet(self::UPDATES_XML_URL); + if ($xml === false || strlen($xml) < 100) { + self::log('Bridge: failed to fetch MokoOnyx updates.xml', 'warning'); + return null; + } + + libxml_use_internal_errors(true); + $doc = simplexml_load_string($xml); + libxml_clear_errors(); + + if (!$doc) { + self::log('Bridge: failed to parse MokoOnyx updates.xml', 'warning'); + return null; + } + + // Find the stable block + foreach ($doc->update as $update) { + $tags = $update->tags->tag ?? []; + foreach ($tags as $tag) { + if ((string) $tag === 'stable') { + foreach ($update->downloads->downloadurl as $dl) { + $format = (string) ($dl['format'] ?? ''); + $url = trim((string) $dl); + if ($format === 'zip' && !empty($url)) { + self::log('Bridge: discovered stable URL: ' . $url); + return $url; + } + } + } + } + } + + self::log('Bridge: no stable ZIP URL found in MokoOnyx updates.xml', 'warning'); + return null; + } + + /** + * HTTP GET helper — tries file_get_contents then cURL. + * + * @return string|false Response body or false on failure. + */ + private static function httpGet(string $url) + { $content = false; - // Method 1: file_get_contents if (ini_get('allow_url_fopen')) { $ctx = stream_context_create([ 'http' => [ @@ -106,12 +188,11 @@ class MokoBridgeMigration 'verify_peer_name' => true, ], ]); - $content = @file_get_contents(self::RELEASE_URL, false, $ctx); + $content = @file_get_contents($url, false, $ctx); } - // Method 2: cURL if ($content === false && function_exists('curl_init')) { - $ch = curl_init(self::RELEASE_URL); + $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, @@ -128,18 +209,7 @@ class MokoBridgeMigration } } - if ($content === false || strlen($content) < 1000) { - self::log('Bridge: failed to download MokoOnyx ZIP from ' . self::RELEASE_URL, 'error'); - return null; - } - - if (file_put_contents($zipPath, $content) === false) { - self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error'); - return null; - } - - self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)'); - return $zipPath; + return $content; } /** diff --git a/src/script.php b/src/script.php index deec459..92e2333 100644 --- a/src/script.php +++ b/src/script.php @@ -113,15 +113,7 @@ class Tpl_MokocassiopeiaInstallerScript ); } - // Bridge migration: MokoCassiopeia → MokoOnyx - $bridgeScript = __DIR__ . '/helper/bridge.php'; - if (is_file($bridgeScript)) { - require_once $bridgeScript; - if (class_exists('MokoBridgeMigration')) { - MokoBridgeMigration::run(); - } - } - + // Bridge migration runs in postflight() — not here — to avoid double execution return true; } From 500bb38162c6981cb7901ce4966dd4061efa3f42 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:02:46 +0000 Subject: [PATCH 13/41] chore: update development SHA-256 for 03.10.14 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index f7ebd47..41481ee 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.14-dev.zip - 62b91d1e23146d038967145d89a7895b3bdc38719ff9d0009d89af7bbc9a211c + a13a7db96357df704a7bc91ee327c2f3f531d99489b5459de34a55f13669faef development Moko Consulting https://mokoconsulting.tech From 116127c7606fae22e0582447ab1bfb39ce363dfa Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:09:11 -0500 Subject: [PATCH 14/41] fix: per-channel updates.xml targeting + release on all patches - auto-update-sha: replace blanket sed with Python targeting only the matching stability channel, fix sha256: prefix to raw hex - auto-release: remove patch 00 skip, all patches now release Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/auto-release.yml | 20 +++---- .github/workflows/auto-update-sha.yml | 79 ++++++++++++++++++--------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 8933815..87f13cf 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -26,7 +26,7 @@ # | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | # | | # | Every version change: archives main -> version/XX.YY branch | -# | Patch 00 = development (no release). First release = patch 01. | +# | All patches release (including 00). Patch 00/01 = full pipeline. | # | First release only (patch == 01): | # | 7b. Create new GitHub Release | # | | @@ -100,19 +100,13 @@ jobs: echo "minor=$MINOR" >> "$GITHUB_OUTPUT" echo "major=$MAJOR" >> "$GITHUB_OUTPUT" echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch 00 = development — skipping release)" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" else - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release — full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch — platform version + badges only)" - fi + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" fi - name: Check if already released diff --git a/.github/workflows/auto-update-sha.yml b/.github/workflows/auto-update-sha.yml index c264abf..d5ce534 100644 --- a/.github/workflows/auto-update-sha.yml +++ b/.github/workflows/auto-update-sha.yml @@ -70,33 +70,60 @@ jobs: echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT echo "SHA-256 Hash: ${SHA256_HASH}" - - name: Update updates.xml + - name: Determine stability channel + id: channel run: | TAG="${{ steps.tag.outputs.tag }}" - SHA256="${{ steps.sha.outputs.sha256 }}" + case "$TAG" in + development) STABILITY="development" ;; + alpha) STABILITY="alpha" ;; + beta) STABILITY="beta" ;; + release-candidate) STABILITY="rc" ;; + *) STABILITY="stable" ;; + esac + echo "stability=${STABILITY}" >> $GITHUB_OUTPUT + echo "Channel: ${STABILITY}" + + - name: Update updates.xml (targeted channel only) + env: + PY_TAG: ${{ steps.tag.outputs.tag }} + PY_SHA: ${{ steps.sha.outputs.sha256 }} + PY_STABILITY: ${{ steps.channel.outputs.stability }} + run: | DATE=$(date +%Y-%m-%d) - - # Update version - sed -i "s|.*|${TAG}|" updates.xml - - # Update creation date - sed -i "s|.*|${DATE}|" updates.xml - - # Update download URL - sed -i "s|.*|https://github.com/${{ github.repository }}/releases/download/${TAG}/mokocassiopeia-src-${TAG}.zip|" updates.xml - - # Update or add SHA-256 hash - if grep -q "" updates.xml; then - sed -i "s|.*|sha256:${SHA256}|" updates.xml - else - # Add SHA-256 after downloadurl - sed -i "/<\/downloadurl>/a\ sha256:${SHA256}<\/sha256>" updates.xml - fi - - echo "Updated updates.xml with:" - echo " Version: ${TAG}" - echo " Date: ${DATE}" - echo " SHA-256: ${SHA256}" + export PY_DATE="$DATE" + + python3 << 'PYEOF' + import re, os + + tag = os.environ["PY_TAG"] + sha256 = os.environ["PY_SHA"] + date = os.environ["PY_DATE"] + stability = os.environ["PY_STABILITY"] + + with open("updates.xml") as f: + content = f.read() + + pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + + if not match: + print(f"No block for {stability} — skipping") + exit(0) + + block = match.group(1) + original = block + + block = re.sub(r"[^<]*", f"{sha256}", block) + block = re.sub(r"[^<]*", f"{date}", block) + + content = content.replace(original, block) + + with open("updates.xml", "w") as f: + f.write(content) + + print(f"Updated {stability} channel: sha={sha256[:16]}..., date={date}") + PYEOF - name: Check for changes id: changes @@ -118,8 +145,10 @@ jobs: git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" + STABILITY="${{ steps.channel.outputs.stability }}" git add updates.xml - git commit -m "chore: Update SHA-256 hash for release ${TAG} - SHA: ${{ steps.sha.outputs.sha256 }}" + git commit -m "chore: update ${STABILITY} SHA-256 for ${TAG} [skip ci]" \ + --author="gitea-actions[bot] " git push origin main From 91e8329c364603a4915528eafe2422b19f9821b6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:23:31 +0000 Subject: [PATCH 15/41] chore: update development SHA-256 for 03.10.14 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 41481ee..056d102 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.14-dev.zip - a13a7db96357df704a7bc91ee327c2f3f531d99489b5459de34a55f13669faef + 3be1a0d2d3b86600f6783f61660dc0bb2f2fc69125192a3124f5e509317d813d development Moko Consulting https://mokoconsulting.tech From 147169cfa0961e8b5d12388aa7cb1e574f7992af Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:26:39 -0500 Subject: [PATCH 16/41] feat: auto-bump patch version in release.yml before building Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 91 +++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 618405f..2d3a669 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,6 +95,77 @@ jobs: echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "Building: ${ZIP_NAME} (${STABILITY})" + - name: Auto-bump patch version + id: bump + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + INPUT_VERSION: ${{ steps.meta.outputs.version }} + INPUT_STABILITY: ${{ steps.meta.outputs.stability }} + INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }} + run: | + # Read current version from README.md + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ -z "$CURRENT" ]; then + echo "No VERSION in README.md — using input version" + echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1) + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1))) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + + echo "Bumping: ${CURRENT} → ${NEW_VERSION}" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md + + # Update templateDetails.xml / manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST" + fi + + # Update only the matching stability channel in updates.xml + if [ -f "updates.xml" ]; then + export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" + python3 << 'PYEOF' + import re, os + old = os.environ["PY_OLD"] + new = os.environ["PY_NEW"] + stability = os.environ["PY_STABILITY"] + with open("updates.xml") as f: + content = f.read() + pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = block.replace(old, new) + content = content.replace(block, updated) + with open("updates.xml", "w") as f: + f.write(content) + print(f"Updated {stability} channel: {old} -> {new}") + PYEOF + fi + + # Commit bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + - name: Install dependencies env: COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' @@ -119,7 +190,7 @@ jobs: - name: Build ZIP id: zip run: | - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" cd build/package zip -r "../${ZIP_NAME}" . cd .. @@ -157,7 +228,7 @@ jobs: id: gitea_release run: | TAG="${{ steps.meta.outputs.tag_name }}" - VERSION="${{ steps.meta.outputs.version }}" + VERSION="${{ steps.bump.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" PRERELEASE="${{ steps.meta.outputs.prerelease }}" SHA256="${{ steps.zip.outputs.sha256 }}" @@ -207,7 +278,7 @@ jobs: - name: "Gitea: Upload ZIP" run: | RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" TOKEN="${{ secrets.GA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" @@ -225,9 +296,9 @@ jobs: continue-on-error: true run: | TAG="${{ steps.meta.outputs.tag_name }}" - VERSION="${{ steps.meta.outputs.version }}" + VERSION="${{ steps.bump.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" SHA256="${{ steps.zip.outputs.sha256 }}" TOKEN="${{ secrets.GH_TOKEN }}" GH_REPO="mokoconsulting-tech/${GITEA_REPO}" @@ -275,9 +346,9 @@ jobs: - name: "Update updates.xml for this channel" run: | STABILITY="${{ steps.meta.outputs.stability }}" - VERSION="${{ steps.meta.outputs.version }}" + VERSION="${{ steps.bump.outputs.version }}" SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" TAG="${{ steps.meta.outputs.tag_name }}" DATE=$(date +%Y-%m-%d) @@ -375,7 +446,7 @@ jobs: fi STABILITY="${{ steps.meta.outputs.stability }}" - VERSION="${{ steps.meta.outputs.version }}" + VERSION="${{ steps.bump.outputs.version }}" CURRENT_BRANCH="${{ github.ref_name }}" TOKEN="${{ secrets.GA_TOKEN }}" @@ -428,9 +499,9 @@ jobs: - name: Summary run: | - VERSION="${{ steps.meta.outputs.version }}" + VERSION="${{ steps.bump.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" SHA256="${{ steps.zip.outputs.sha256 }}" TAG="${{ steps.meta.outputs.tag_name }}" From c547e064ae97a10e1bc0ee4404387a4c653b2fee Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:27:35 +0000 Subject: [PATCH 17/41] =?UTF-8?q?chore(version):=20bump=2003.10.14=20?= =?UTF-8?q?=E2=86=92=2003.10.15=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a309ee3..e18e8bd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.14 + VERSION: 03.10.15 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index f35dcad..703c5f4 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.14 + 03.10.15 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 056d102..60d9df3 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.14 + 03.10.15 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.14-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.15-dev.zip 3be1a0d2d3b86600f6783f61660dc0bb2f2fc69125192a3124f5e509317d813d development From 2481b63001f0946b2313af839a92b77023a50eab Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:27:39 +0000 Subject: [PATCH 18/41] chore: update development SHA-256 for 03.10.15 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 60d9df3..a98b1c6 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.15-dev.zip - 3be1a0d2d3b86600f6783f61660dc0bb2f2fc69125192a3124f5e509317d813d + eded1898779d6fd37025be8c9e076d31fbe51f456ddc10bb803cef83d239b32d development Moko Consulting https://mokoconsulting.tech From 300ce0d61f421d76c4f3d954b336a6f8646584e0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:30:07 -0500 Subject: [PATCH 19/41] docs: update version management for auto-bump and multi-channel updates.xml Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/CLAUDE.md | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 9803910..be50cc5 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -115,38 +115,34 @@ BRIEF: One-line description **`README.md` is the single source of truth for the repository version.** -- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- **Patch version is auto-bumped by the release workflow** — `release.yml` reads the current version from `README.md`, increments the patch (`XX.YY.ZZ` → `XX.YY.(ZZ+1)`), updates `README.md`, `templateDetails.xml`, and the matching channel in `updates.xml`, commits, pushes, then builds the ZIP. Manual bumping is no longer required. - The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references. - Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`). - Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. ### Joomla Version Alignment -The version in `README.md` **must always match** the `` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically. +The version in `README.md` **must always match** the `` tag in `templateDetails.xml` and the matching channel entry in `updates.xml`. The release workflow updates all three automatically. + +### Multi-Channel updates.xml + +`updates.xml` contains separate `` blocks per stability channel (development, alpha, beta, rc, stable). Each release workflow only modifies its own channel using targeted Python regex replacement — other channels are preserved untouched. Joomla filters by the user's "Minimum Stability" setting. ```xml - -01.02.04 - - - - {{EXTENSION_NAME}} - 01.02.04 - - - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip - - - - - + ...development... + ...alpha... + ...beta... + ...rc... + ...stable... ``` +**Key rules:** +- SHA-256 must be raw hex (no `sha256:` prefix) +- Version format must be `XX.YY.ZZ`, not tag names like `v01` +- Download URLs must point to Gitea (not GitHub) for all pre-release channels + --- ## Joomla Extension Structure @@ -286,11 +282,11 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d | Change type | Documentation to update | |-------------|------------------------| | New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | -| New or changed manifest.xml | Update `updates.xml` version; bump README.md version | -| New release | Prepend `` block to `updates.xml`; update CHANGELOG.md; bump README.md version | +| New or changed manifest.xml | Release workflow auto-bumps version across README.md, templateDetails.xml, and updates.xml | +| New release | Trigger `release.yml` — auto-bumps patch, builds ZIP, updates matching channel in `updates.xml` | | New or changed workflow | `docs/workflows/.md` | | Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it | +| **Every release** | **Patch auto-bumped** by `release.yml` — no manual version bump needed | --- From 9e257d16ed78d3ab9bb4a97e1fdd4a52a29a96a0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:31:50 -0500 Subject: [PATCH 20/41] Bridge: rewrite as rename-in-place + DB update (no external downloads) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename templates/mokocassiopeia → mokoonyx - Rename media/templates/site/mokocassiopeia → mokoonyx - Update #__extensions element and name - Update #__template_styles template, title, and params - Update #__menu link references - Update #__update_sites to point to MokoOnyx updates.xml - Clear #__updates cached entries for old extension - No HTTP requests, no ZIP downloads, no Installer conflicts Co-Authored-By: Claude Opus 4.6 (1M context) --- src/helper/bridge.php | 458 ++++++++++++++---------------------------- 1 file changed, 153 insertions(+), 305 deletions(-) diff --git a/src/helper/bridge.php b/src/helper/bridge.php index 9172027..34f14fe 100644 --- a/src/helper/bridge.php +++ b/src/helper/bridge.php @@ -10,14 +10,13 @@ /** * Bridge migration helper — MokoCassiopeia → MokoOnyx * - * Downloads and installs MokoOnyx from the Gitea release, then migrates - * template styles and menu assignments from MokoCassiopeia. + * Renames template files/folders and updates the database to migrate + * from MokoCassiopeia to MokoOnyx. No external downloads required. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; -use Joomla\CMS\Installer\Installer; use Joomla\CMS\Log\Log; class MokoBridgeMigration @@ -28,12 +27,6 @@ class MokoBridgeMigration private const OLD_DISPLAY = 'MokoCassiopeia'; private const NEW_DISPLAY = 'MokoOnyx'; - /** Raw URL for MokoOnyx updates.xml on main — used to discover the stable download URL */ - private const UPDATES_XML_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; - - /** Fallback URL if updates.xml cannot be parsed */ - private const FALLBACK_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip'; - /** * Run the full migration. */ @@ -41,45 +34,32 @@ class MokoBridgeMigration { $app = Factory::getApplication(); - // Check if MokoOnyx is already installed + // Already migrated? if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { - self::log('MokoOnyx already installed — skipping download.'); - self::migrateStyles(); + self::log('MokoOnyx template dir already exists — updating database only.'); + self::updateDatabase(); self::notifyUser($app); return true; } - // 1. Try downloading and installing MokoOnyx from Gitea release - $installed = false; - $zipPath = self::downloadRelease(); - if ($zipPath) { - $installed = self::installPackage($zipPath); - @unlink($zipPath); - } - - // 2. Fallback: copy from MokoCassiopeia and rename - if (!$installed) { - self::log('Bridge: download/install failed, falling back to file copy'); - $installed = self::copyAndRename(); - } - - if (!$installed) { + // 1. Rename template directory + $renamed = self::renameTemplateDir(); + if (!$renamed) { $app->enqueueMessage( - 'MokoOnyx migration: automatic installation failed. ' - . 'Please install MokoOnyx manually from ' - . 'Gitea Releases.', + 'MokoOnyx migration: could not rename template directory. ' + . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', 'warning' ); return false; } - // 3. Copy user files (custom themes, user.css, user.js) - self::copyAndRename(); + // 2. Rename media directory + self::renameMediaDir(); - // 4. Migrate template styles and params - self::migrateStyles(); + // 3. Update database (extensions, template_styles, menu assignments) + self::updateDatabase(); - // 5. Notify admin + // 4. Notify admin self::notifyUser($app); self::log('Bridge migration completed successfully.'); @@ -87,308 +67,176 @@ class MokoBridgeMigration } /** - * Download the MokoOnyx ZIP to Joomla's tmp directory. - * - * Reads MokoOnyx's updates.xml on main to discover the current stable - * download URL, falling back to a hardcoded URL if parsing fails. + * Rename templates/mokocassiopeia → templates/mokoonyx */ - private static function downloadRelease(): ?string + private static function renameTemplateDir(): bool { - $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); - $zipPath = $tmpDir . '/mokoonyx-install.zip'; + $oldDir = JPATH_ROOT . '/templates/' . self::OLD_NAME; + $newDir = JPATH_ROOT . '/templates/' . self::NEW_NAME; - // 1. Discover the stable download URL from MokoOnyx's updates.xml - $releaseUrl = self::discoverStableUrl(); - if (!$releaseUrl) { - self::log('Bridge: could not discover release URL from updates.xml, using fallback'); - $releaseUrl = self::FALLBACK_URL; - } - - self::log('Bridge: downloading MokoOnyx from ' . $releaseUrl); - - // 2. Download the ZIP - $content = self::httpGet($releaseUrl); - - if ($content === false || strlen($content) < 1000) { - self::log('Bridge: failed to download MokoOnyx ZIP from ' . $releaseUrl, 'error'); - return null; - } - - if (file_put_contents($zipPath, $content) === false) { - self::log('Bridge: failed to write ZIP to ' . $zipPath, 'error'); - return null; - } - - self::log('Bridge: downloaded MokoOnyx ZIP (' . strlen($content) . ' bytes)'); - return $zipPath; - } - - /** - * Fetch MokoOnyx's updates.xml and extract the stable channel ZIP URL. - * - * Always targets the stable channel — the bridge should only install - * production-ready builds of MokoOnyx. - */ - private static function discoverStableUrl(): ?string - { - $xml = self::httpGet(self::UPDATES_XML_URL); - if ($xml === false || strlen($xml) < 100) { - self::log('Bridge: failed to fetch MokoOnyx updates.xml', 'warning'); - return null; - } - - libxml_use_internal_errors(true); - $doc = simplexml_load_string($xml); - libxml_clear_errors(); - - if (!$doc) { - self::log('Bridge: failed to parse MokoOnyx updates.xml', 'warning'); - return null; - } - - // Find the stable block - foreach ($doc->update as $update) { - $tags = $update->tags->tag ?? []; - foreach ($tags as $tag) { - if ((string) $tag === 'stable') { - foreach ($update->downloads->downloadurl as $dl) { - $format = (string) ($dl['format'] ?? ''); - $url = trim((string) $dl); - if ($format === 'zip' && !empty($url)) { - self::log('Bridge: discovered stable URL: ' . $url); - return $url; - } - } - } - } - } - - self::log('Bridge: no stable ZIP URL found in MokoOnyx updates.xml', 'warning'); - return null; - } - - /** - * HTTP GET helper — tries file_get_contents then cURL. - * - * @return string|false Response body or false on failure. - */ - private static function httpGet(string $url) - { - $content = false; - - if (ini_get('allow_url_fopen')) { - $ctx = stream_context_create([ - 'http' => [ - 'timeout' => 60, - 'follow_location' => true, - 'max_redirects' => 5, - ], - 'ssl' => [ - 'verify_peer' => true, - 'verify_peer_name' => true, - ], - ]); - $content = @file_get_contents($url, false, $ctx); - } - - if ($content === false && function_exists('curl_init')) { - $ch = curl_init($url); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 5, - CURLOPT_TIMEOUT => 60, - CURLOPT_SSL_VERIFYPEER => true, - ]); - $content = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200) { - $content = false; - } - } - - return $content; - } - - /** - * Install the downloaded ZIP via Joomla's Installer. - */ - private static function installPackage(string $zipPath): bool - { - try { - $installer = Installer::getInstance(); - - $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); - $extractDir = $tmpDir . '/mokoonyx_install_' . time(); - - $zip = new \ZipArchive(); - if ($zip->open($zipPath) !== true) { - self::log('Bridge: failed to open ZIP', 'error'); - return false; - } - $zip->extractTo($extractDir); - $zip->close(); - - $result = $installer->install($extractDir); - - if (is_dir($extractDir)) { - self::removeDirectory($extractDir); - } - - if ($result) { - self::log('Bridge: MokoOnyx installed via Joomla Installer'); - } else { - self::log('Bridge: Joomla Installer returned false', 'error'); - } - - return (bool) $result; - } catch (\Throwable $e) { - self::log('Bridge: install failed: ' . $e->getMessage(), 'error'); + if (!is_dir($oldDir)) { + self::log('Bridge: old template dir not found: ' . $oldDir, 'warning'); return false; } + + if (is_dir($newDir)) { + self::log('Bridge: new template dir already exists — skipping rename'); + return true; + } + + $result = @rename($oldDir, $newDir); + if ($result) { + self::log('Bridge: renamed template dir to ' . self::NEW_NAME); + } else { + self::log('Bridge: failed to rename template dir', 'error'); + } + + return $result; } /** - * Migrate template styles and menu assignments from MokoCassiopeia to MokoOnyx. + * Rename media/templates/site/mokocassiopeia → mokoonyx */ - private static function migrateStyles(): void - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select('*') - ->from('#__template_styles') - ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)) - ->where($db->quoteName('client_id') . ' = 0'); - $oldStyles = $db->setQuery($query)->loadObjectList(); - - if (empty($oldStyles)) { - self::log('No MokoCassiopeia styles found — nothing to migrate.'); - return; - } - - foreach ($oldStyles as $oldStyle) { - $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__template_styles') - ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle)); - if ((int) $db->setQuery($query)->loadResult() > 0) { - continue; - } - - $newStyle = clone $oldStyle; - unset($newStyle->id); - $newStyle->template = self::NEW_NAME; - $newStyle->title = $newTitle; - - if (is_string($newStyle->params)) { - $newStyle->params = str_replace(self::OLD_NAME, self::NEW_NAME, $newStyle->params); - } - - $db->insertObject('#__template_styles', $newStyle, 'id'); - $newId = $newStyle->id; - - if ($oldStyle->home == 1) { - $db->setQuery( - $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 1') - ->where('id = ' . (int) $newId) - )->execute(); - - $db->setQuery( - $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 0') - ->where('id = ' . (int) $oldStyle->id) - )->execute(); - - self::log('Set MokoOnyx as default site template.'); - } - } - - self::log('Migrated ' . count($oldStyles) . ' template style(s).'); - } - - /** - * Copy user-specific files from MokoCassiopeia to MokoOnyx. - * Only copies custom themes, user.css, and user.js — not the full template. - * MokoOnyx must already be installed (via download or manual). - */ - private static function copyAndRename(): bool + private static function renameMediaDir(): void { $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; - if (!is_dir($newMedia)) { - self::log('Bridge: MokoOnyx media dir not found — cannot copy user files', 'warning'); - return false; + if (!is_dir($oldMedia)) { + self::log('Bridge: old media dir not found — skipping'); + return; } - $copied = 0; + if (is_dir($newMedia)) { + self::log('Bridge: new media dir already exists — skipping rename'); + return; + } - // Copy custom theme palettes - $userFiles = [ - 'css/theme/light.custom.css', - 'css/theme/dark.custom.css', - 'css/theme/light.custom.min.css', - 'css/theme/dark.custom.min.css', - 'css/user.css', - 'css/user.min.css', - 'js/user.js', - 'js/user.min.js', - ]; + if (@rename($oldMedia, $newMedia)) { + self::log('Bridge: renamed media dir to ' . self::NEW_NAME); + } else { + self::log('Bridge: failed to rename media dir', 'warning'); + } + } - foreach ($userFiles as $relPath) { - $srcFile = $oldMedia . '/' . $relPath; - $dstFile = $newMedia . '/' . $relPath; - if (is_file($srcFile) && !is_file($dstFile)) { - $dstDir = dirname($dstFile); - if (!is_dir($dstDir)) { - mkdir($dstDir, 0755, true); - } - copy($srcFile, $dstFile); - $copied++; + /** + * Update all database references from mokocassiopeia → mokoonyx. + */ + private static function updateDatabase(): void + { + $db = Factory::getDbo(); + + // 1. Update #__extensions — change element and name + $query = $db->getQuery(true) + ->update('#__extensions') + ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) + ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + try { + $db->setQuery($query)->execute(); + $affected = $db->getAffectedRows(); + if ($affected > 0) { + self::log("Bridge: updated {$affected} row(s) in #__extensions"); + } + } catch (\Throwable $e) { + self::log('Bridge: #__extensions update failed: ' . $e->getMessage(), 'error'); + } + + // 2. Update #__template_styles — rename template and title + $query = $db->getQuery(true) + ->select('*') + ->from('#__template_styles') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)); + $styles = $db->setQuery($query)->loadObjectList(); + + foreach ($styles as $style) { + $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); + // Also catch lowercase variant + $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); + + $newParams = $style->params; + if (is_string($newParams)) { + $newParams = str_replace(self::OLD_NAME, self::NEW_NAME, $newParams); + } + + $update = $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) + ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) + ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) + ->where('id = ' . (int) $style->id); + + try { + $db->setQuery($update)->execute(); + } catch (\Throwable $e) { + self::log('Bridge: style update failed for id=' . $style->id . ': ' . $e->getMessage(), 'error'); } } - // Copy favicon directory - $faviconSrc = JPATH_ROOT . '/images/favicons'; - if (is_dir($faviconSrc)) { - self::log('Bridge: favicons already at images/favicons — shared between templates'); + if (!empty($styles)) { + self::log('Bridge: updated ' . count($styles) . ' template style(s) in #__template_styles'); } - self::log("Bridge: copied {$copied} user file(s) to MokoOnyx"); - return true; + // 3. Update #__menu — fix template_style_id link field references + // Menu items store the template name in the link for template-specific assignments + try { + $query = $db->getQuery(true) + ->update('#__menu') + ->set($db->quoteName('link') . ' = REPLACE(' . $db->quoteName('link') . ', ' + . $db->quote(self::OLD_NAME) . ', ' . $db->quote(self::NEW_NAME) . ')') + ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%')); + $db->setQuery($query)->execute(); + $affected = $db->getAffectedRows(); + if ($affected > 0) { + self::log("Bridge: updated {$affected} menu link(s)"); + } + } catch (\Throwable $e) { + self::log('Bridge: #__menu update failed: ' . $e->getMessage(), 'warning'); + } + + // 4. Update #__update_sites — point to MokoOnyx updates.xml + try { + $newLocation = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; + $query = $db->getQuery(true) + ->update('#__update_sites') + ->set($db->quoteName('location') . ' = ' . $db->quote($newLocation)) + ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY)) + ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); + $db->setQuery($query)->execute(); + $affected = $db->getAffectedRows(); + if ($affected > 0) { + self::log("Bridge: updated {$affected} update site(s) to MokoOnyx"); + } + } catch (\Throwable $e) { + self::log('Bridge: #__update_sites update failed: ' . $e->getMessage(), 'warning'); + } + + // 5. Update #__updates — clear cached updates for old extension + try { + $query = $db->getQuery(true) + ->delete('#__updates') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); + $db->setQuery($query)->execute(); + $affected = $db->getAffectedRows(); + if ($affected > 0) { + self::log("Bridge: cleared {$affected} cached update(s) for old extension"); + } + } catch (\Throwable $e) { + self::log('Bridge: #__updates cleanup failed: ' . $e->getMessage(), 'warning'); + } } private static function notifyUser($app): void { $app->enqueueMessage( 'MokoCassiopeia has been renamed to MokoOnyx.
' - . 'Your template settings have been migrated automatically. ' - . 'MokoOnyx is now your active site template. ' - . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', + . 'Your template files, settings, and menu assignments have been migrated automatically. ' + . 'MokoOnyx is now your active site template.', 'success' ); } - private static function removeDirectory(string $dir): void - { - $items = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($items as $item) { - $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); - } - rmdir($dir); - } - private static function log(string $message, string $priority = 'info'): void { $priorities = [ From 45d70d4d18d2cfefb913b2e5239386cd0b16616f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:32:23 +0000 Subject: [PATCH 21/41] =?UTF-8?q?chore(version):=20bump=2003.10.15=20?= =?UTF-8?q?=E2=86=92=2003.10.16=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e18e8bd..7247831 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.15 + VERSION: 03.10.16 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 703c5f4..9dad4ff 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.15 + 03.10.16 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index a98b1c6..7b6407b 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.15 + 03.10.16 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.15-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.16-dev.zip eded1898779d6fd37025be8c9e076d31fbe51f456ddc10bb803cef83d239b32d development From 6eea11cf961b168861308bfaf42fbb87412a6385 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:32:26 +0000 Subject: [PATCH 22/41] chore: update development SHA-256 for 03.10.16 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 7b6407b..dc0a606 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.16-dev.zip - eded1898779d6fd37025be8c9e076d31fbe51f456ddc10bb803cef83d239b32d + 089563017322317f989c49f8260d6f84cf2b84235cad4584504b716b9c429e83 development Moko Consulting https://mokoconsulting.tech From 3fb48da7c7cc4c7e2e78b0a9f23a2acfa0e84c41 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:34:50 -0500 Subject: [PATCH 23/41] Bridge: point dev download URL to MokoOnyx stable release Joomla will install MokoOnyx directly when updating MokoCassiopeia. Co-Authored-By: Claude Opus 4.6 (1M context) --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index dc0a606..ad6c112 100644 --- a/updates.xml +++ b/updates.xml @@ -17,7 +17,7 @@ 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.16-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip 089563017322317f989c49f8260d6f84cf2b84235cad4584504b716b9c429e83 development From 9b43bef0bdc1478a668e107f13681110f919fc25 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:45:21 -0500 Subject: [PATCH 24/41] Bridge: redirect all channels to matching MokoOnyx streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev → MokoOnyx/development - alpha → MokoOnyx/alpha - beta → MokoOnyx/beta - rc → MokoOnyx/release-candidate - stable → MokoOnyx/v01 Co-Authored-By: Claude Opus 4.6 (1M context) --- updates.xml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/updates.xml b/updates.xml index ad6c112..4fe0a5c 100644 --- a/updates.xml +++ b/updates.xml @@ -17,7 +17,7 @@ 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.01-dev.zip 089563017322317f989c49f8260d6f84cf2b84235cad4584504b716b9c429e83 development @@ -38,9 +38,9 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/mokoonyx-01.00.00.zip - fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 alpha Moko Consulting https://mokoconsulting.tech @@ -59,9 +59,9 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/mokoonyx-01.00.00.zip - fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 beta Moko Consulting https://mokoconsulting.tech @@ -80,10 +80,9 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.00.zip - fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 rc Moko Consulting https://mokoconsulting.tech @@ -102,10 +101,9 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip - fac226ed62e79a01dd01b6184e832a4e8c927c7b7a5ac18cc645332cd87af33d + 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 stable Moko Consulting https://mokoconsulting.tech From 122a84b5a9483915b6a3233aa6a67a56469d6d95 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:47:22 -0500 Subject: [PATCH 25/41] Bump 03.10.17 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7247831..ba74520 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.16 + VERSION: 03.10.17 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 9dad4ff..c14bcf8 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.16 + 03.10.17 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 4fe0a5c..b4cc69c 100644 --- a/updates.xml +++ b/updates.xml @@ -13,7 +13,7 @@ mokocassiopeia template site - 03.10.16 + 03.10.17 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development From e4d07f05b13be1541937c850cd3493d418a1f280 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:53:11 -0500 Subject: [PATCH 26/41] Bridge: embed in script.php, rename-in-place (no downloads) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote bridge from scratch as part of script.php postflight(): 1. Rename templates/mokocassiopeia → mokoonyx 2. Rename media/templates/site/mokocassiopeia → mokoonyx 3. Update #__extensions element + name 4. Update all #__template_styles (template, title, params) 5. Redirect #__update_sites to MokoOnyx updates.xml 6. Clear #__updates cache No HTTP requests, no ZIP downloads, no separate bridge.php file. Reverted updates.xml download URLs back to MokoCassiopeia releases. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/helper/bridge.php | 256 ---------------------------- src/script.php | 385 ++++++++++++++++++++++++------------------ updates.xml | 17 +- 3 files changed, 223 insertions(+), 435 deletions(-) delete mode 100644 src/helper/bridge.php diff --git a/src/helper/bridge.php b/src/helper/bridge.php deleted file mode 100644 index 34f14fe..0000000 --- a/src/helper/bridge.php +++ /dev/null @@ -1,256 +0,0 @@ - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -/** - * Bridge migration helper — MokoCassiopeia → MokoOnyx - * - * Renames template files/folders and updates the database to migrate - * from MokoCassiopeia to MokoOnyx. No external downloads required. - */ - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\CMS\Log\Log; - -class MokoBridgeMigration -{ - private const OLD_NAME = 'mokocassiopeia'; - private const NEW_NAME = 'mokoonyx'; - - private const OLD_DISPLAY = 'MokoCassiopeia'; - private const NEW_DISPLAY = 'MokoOnyx'; - - /** - * Run the full migration. - */ - public static function run(): bool - { - $app = Factory::getApplication(); - - // Already migrated? - if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { - self::log('MokoOnyx template dir already exists — updating database only.'); - self::updateDatabase(); - self::notifyUser($app); - return true; - } - - // 1. Rename template directory - $renamed = self::renameTemplateDir(); - if (!$renamed) { - $app->enqueueMessage( - 'MokoOnyx migration: could not rename template directory. ' - . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', - 'warning' - ); - return false; - } - - // 2. Rename media directory - self::renameMediaDir(); - - // 3. Update database (extensions, template_styles, menu assignments) - self::updateDatabase(); - - // 4. Notify admin - self::notifyUser($app); - - self::log('Bridge migration completed successfully.'); - return true; - } - - /** - * Rename templates/mokocassiopeia → templates/mokoonyx - */ - private static function renameTemplateDir(): bool - { - $oldDir = JPATH_ROOT . '/templates/' . self::OLD_NAME; - $newDir = JPATH_ROOT . '/templates/' . self::NEW_NAME; - - if (!is_dir($oldDir)) { - self::log('Bridge: old template dir not found: ' . $oldDir, 'warning'); - return false; - } - - if (is_dir($newDir)) { - self::log('Bridge: new template dir already exists — skipping rename'); - return true; - } - - $result = @rename($oldDir, $newDir); - if ($result) { - self::log('Bridge: renamed template dir to ' . self::NEW_NAME); - } else { - self::log('Bridge: failed to rename template dir', 'error'); - } - - return $result; - } - - /** - * Rename media/templates/site/mokocassiopeia → mokoonyx - */ - private static function renameMediaDir(): void - { - $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; - $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; - - if (!is_dir($oldMedia)) { - self::log('Bridge: old media dir not found — skipping'); - return; - } - - if (is_dir($newMedia)) { - self::log('Bridge: new media dir already exists — skipping rename'); - return; - } - - if (@rename($oldMedia, $newMedia)) { - self::log('Bridge: renamed media dir to ' . self::NEW_NAME); - } else { - self::log('Bridge: failed to rename media dir', 'warning'); - } - } - - /** - * Update all database references from mokocassiopeia → mokoonyx. - */ - private static function updateDatabase(): void - { - $db = Factory::getDbo(); - - // 1. Update #__extensions — change element and name - $query = $db->getQuery(true) - ->update('#__extensions') - ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) - ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - try { - $db->setQuery($query)->execute(); - $affected = $db->getAffectedRows(); - if ($affected > 0) { - self::log("Bridge: updated {$affected} row(s) in #__extensions"); - } - } catch (\Throwable $e) { - self::log('Bridge: #__extensions update failed: ' . $e->getMessage(), 'error'); - } - - // 2. Update #__template_styles — rename template and title - $query = $db->getQuery(true) - ->select('*') - ->from('#__template_styles') - ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)); - $styles = $db->setQuery($query)->loadObjectList(); - - foreach ($styles as $style) { - $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); - // Also catch lowercase variant - $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); - - $newParams = $style->params; - if (is_string($newParams)) { - $newParams = str_replace(self::OLD_NAME, self::NEW_NAME, $newParams); - } - - $update = $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) - ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) - ->where('id = ' . (int) $style->id); - - try { - $db->setQuery($update)->execute(); - } catch (\Throwable $e) { - self::log('Bridge: style update failed for id=' . $style->id . ': ' . $e->getMessage(), 'error'); - } - } - - if (!empty($styles)) { - self::log('Bridge: updated ' . count($styles) . ' template style(s) in #__template_styles'); - } - - // 3. Update #__menu — fix template_style_id link field references - // Menu items store the template name in the link for template-specific assignments - try { - $query = $db->getQuery(true) - ->update('#__menu') - ->set($db->quoteName('link') . ' = REPLACE(' . $db->quoteName('link') . ', ' - . $db->quote(self::OLD_NAME) . ', ' . $db->quote(self::NEW_NAME) . ')') - ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%')); - $db->setQuery($query)->execute(); - $affected = $db->getAffectedRows(); - if ($affected > 0) { - self::log("Bridge: updated {$affected} menu link(s)"); - } - } catch (\Throwable $e) { - self::log('Bridge: #__menu update failed: ' . $e->getMessage(), 'warning'); - } - - // 4. Update #__update_sites — point to MokoOnyx updates.xml - try { - $newLocation = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; - $query = $db->getQuery(true) - ->update('#__update_sites') - ->set($db->quoteName('location') . ' = ' . $db->quote($newLocation)) - ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY)) - ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); - $db->setQuery($query)->execute(); - $affected = $db->getAffectedRows(); - if ($affected > 0) { - self::log("Bridge: updated {$affected} update site(s) to MokoOnyx"); - } - } catch (\Throwable $e) { - self::log('Bridge: #__update_sites update failed: ' . $e->getMessage(), 'warning'); - } - - // 5. Update #__updates — clear cached updates for old extension - try { - $query = $db->getQuery(true) - ->delete('#__updates') - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); - $db->setQuery($query)->execute(); - $affected = $db->getAffectedRows(); - if ($affected > 0) { - self::log("Bridge: cleared {$affected} cached update(s) for old extension"); - } - } catch (\Throwable $e) { - self::log('Bridge: #__updates cleanup failed: ' . $e->getMessage(), 'warning'); - } - } - - private static function notifyUser($app): void - { - $app->enqueueMessage( - 'MokoCassiopeia has been renamed to MokoOnyx.
' - . 'Your template files, settings, and menu assignments have been migrated automatically. ' - . 'MokoOnyx is now your active site template.', - 'success' - ); - } - - private static function log(string $message, string $priority = 'info'): void - { - $priorities = [ - 'info' => Log::INFO, - 'warning' => Log::WARNING, - 'error' => Log::ERROR, - ]; - - Log::addLogger( - ['text_file' => 'mokocassiopeia_bridge.log.php'], - Log::ALL, - ['mokocassiopeia_bridge'] - ); - - Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia_bridge'); - } -} diff --git a/src/script.php b/src/script.php index 92e2333..b379812 100644 --- a/src/script.php +++ b/src/script.php @@ -1,6 +1,6 @@ + * Copyright (C) 2026 Moko Consulting * * This file is part of a Moko Consulting project. * @@ -8,12 +8,11 @@ */ /** - * Template install/update/uninstall script. - * Joomla calls the methods in this class automatically during template - * install, update, and uninstall via the element in - * templateDetails.xml. - * Joomla 5 and 6 compatible — uses the InstallerScriptInterface when - * available, falls back to the legacy class-based approach otherwise. + * MokoCassiopeia install/update/uninstall script. + * + * On update: renames the template to MokoOnyx (dirs + database), + * copies all style params, creates matching styles, copies user files, + * and redirects the update server. No external downloads needed. */ defined('_JEXEC') or die; @@ -24,33 +23,23 @@ use Joomla\CMS\Log\Log; class Tpl_MokocassiopeiaInstallerScript { - /** - * Minimum PHP version required by this template. - */ - private const MIN_PHP = '8.1.0'; - - /** - * Minimum Joomla version required by this template. - */ + private const MIN_PHP = '8.1.0'; private const MIN_JOOMLA = '4.4.0'; - /** - * Called before install/update/uninstall. - * - * @param string $type install, update, discover_install, or uninstall. - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return bool True to proceed, false to abort. - */ + private const OLD_NAME = 'mokocassiopeia'; + private const NEW_NAME = 'mokoonyx'; + private const OLD_DISPLAY = 'MokoCassiopeia'; + private const NEW_DISPLAY = 'MokoOnyx'; + + private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; + + // ── Joomla lifecycle methods ─────────────────────────────────────── + public function preflight(string $type, InstallerAdapter $parent): bool { if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) { Factory::getApplication()->enqueueMessage( - sprintf( - 'MokoCassiopeia requires PHP %s or later. You are running PHP %s.', - self::MIN_PHP, - PHP_VERSION - ), + sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION), 'error' ); return false; @@ -58,11 +47,7 @@ class Tpl_MokocassiopeiaInstallerScript if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) { Factory::getApplication()->enqueueMessage( - sprintf( - 'MokoCassiopeia requires Joomla %s or later. You are running Joomla %s.', - self::MIN_JOOMLA, - JVERSION - ), + sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION), 'error' ); return false; @@ -71,167 +56,231 @@ class Tpl_MokocassiopeiaInstallerScript return true; } - /** - * Called after a successful install. - * - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return bool - */ public function install(InstallerAdapter $parent): bool { - $this->logMessage('MokoCassiopeia template installed.'); + $this->log('MokoCassiopeia installed.'); return true; } - /** - * Called after a successful update. - * - * This is where the CSS variable sync runs — it detects variables that - * were added in the new version and injects them into the user's custom - * palette files without overwriting existing values. - * - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return bool - */ public function update(InstallerAdapter $parent): bool { - $this->logMessage('MokoCassiopeia update() called — version ' . ($parent->getManifest()->version ?? 'unknown')); - - // Run CSS variable sync to inject any new variables into user's custom palettes. - $synced = $this->syncCustomVariables($parent); - - if ($synced > 0) { - Factory::getApplication()->enqueueMessage( - sprintf( - 'MokoCassiopeia: %d new CSS variable(s) were added to your custom palette files. ' - . 'Review them in your light.custom.css and/or dark.custom.css to customise the new defaults.', - $synced - ), - 'notice' - ); - } - - // Bridge migration runs in postflight() — not here — to avoid double execution + $this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?')); return true; } - /** - * Called after a successful uninstall. - * - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return bool - */ public function uninstall(InstallerAdapter $parent): bool { - $this->logMessage('MokoCassiopeia template uninstalled.'); + $this->log('MokoCassiopeia uninstalled.'); return true; } - /** - * Called after install/update completes (regardless of type). - * - * @param string $type install, update, or discover_install. - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return bool - */ public function postflight(string $type, InstallerAdapter $parent): bool { - // Bridge migration runs in postflight (more reliable than update() for templates) if ($type === 'update') { - $bridgeScript = $parent->getParent()->getPath('source') . '/helper/bridge.php'; - if (!is_file($bridgeScript)) { - $bridgeScript = __DIR__ . '/helper/bridge.php'; - } - if (is_file($bridgeScript)) { - require_once $bridgeScript; - if (class_exists('MokoBridgeMigration')) { - $this->logMessage('Running MokoOnyx bridge migration from postflight...'); - MokoBridgeMigration::run(); - } - } + $this->log('=== MokoCassiopeia → MokoOnyx bridge ==='); + $this->bridge(); } return true; } - /** - * Run the CSS variable sync utility. - * - * Loads sync_custom_vars.php from the template directory and calls - * MokoCssVarSync::run() to detect and inject missing variables. - * - * @param InstallerAdapter $parent The adapter calling this method. - * - * @return int Number of variables added across all files. - */ - private function syncCustomVariables(InstallerAdapter $parent): int + // ── Bridge: rename-in-place + DB migration ───────────────────────── + + private function bridge(): void { - $templateDir = $parent->getParent()->getPath('source'); + $app = Factory::getApplication(); - // The sync script lives alongside this script in the template root. - $syncScript = $templateDir . '/sync_custom_vars.php'; - - if (!is_file($syncScript)) { - $this->logMessage('CSS variable sync script not found at: ' . $syncScript, 'warning'); - return 0; - } - - require_once $syncScript; - - if (!class_exists('MokoCssVarSync')) { - $this->logMessage('MokoCssVarSync class not found after loading script.', 'warning'); - return 0; - } - - try { - $joomlaRoot = JPATH_ROOT; - $results = MokoCssVarSync::run($joomlaRoot); - - $totalAdded = 0; - foreach ($results as $filePath => $result) { - $totalAdded += count($result['added']); - if (!empty($result['added'])) { - $this->logMessage( - sprintf( - 'CSS sync: added %d variable(s) to %s', - count($result['added']), - basename($filePath) - ) - ); - } - } - - return $totalAdded; - } catch (\Throwable $e) { - $this->logMessage('CSS variable sync failed: ' . $e->getMessage(), 'error'); - return 0; - } - } - - /** - * Log a message to Joomla's log system. - * - * @param string $message The log message. - * @param string $priority Log priority (info, warning, error). - */ - private function logMessage(string $message, string $priority = 'info'): void - { - $priorities = [ - 'info' => Log::INFO, - 'warning' => Log::WARNING, - 'error' => Log::ERROR, - ]; - - Log::addLogger( - ['text_file' => 'mokocassiopeia.log.php'], - Log::ALL, - ['mokocassiopeia'] + // 1. Rename template directory + $templateRenamed = $this->renameDir( + JPATH_ROOT . '/templates/' . self::OLD_NAME, + JPATH_ROOT . '/templates/' . self::NEW_NAME, + 'template' ); - Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia'); + if (!$templateRenamed && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { + $app->enqueueMessage( + 'Could not rename template directory to MokoOnyx. ' + . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', + 'warning' + ); + return; + } + + // 2. Rename media directory + $this->renameDir( + JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME, + JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME, + 'media' + ); + + // 3. Update #__extensions + $this->updateExtensions(); + + // 4. Migrate template styles (create matching MokoOnyx styles with same params) + $this->migrateStyles(); + + // 5. Copy user files (custom themes, user.css, user.js) + $this->copyUserFiles(); + + // 6. Redirect update server to MokoOnyx + $this->updateUpdateServer(); + + // 7. Notify + $app->enqueueMessage( + 'MokoCassiopeia has been renamed to MokoOnyx.
' + . 'All template settings, styles, and custom files have been migrated. ' + . 'MokoOnyx is now your active site template.', + 'success' + ); + + $this->log('=== Bridge completed ==='); + } + + // ── Bridge helpers ───────────────────────────────────────────────── + + private function renameDir(string $old, string $new, string $label): bool + { + if (!is_dir($old)) { + $this->log("Bridge: {$label} dir not found ({$old}) — skipping."); + return false; + } + + if (is_dir($new)) { + $this->log("Bridge: {$label} dir already exists ({$new}) — skipping rename."); + return true; + } + + if (@rename($old, $new)) { + $this->log("Bridge: renamed {$label} dir → " . self::NEW_NAME); + return true; + } + + $this->log("Bridge: failed to rename {$label} dir.", 'error'); + return false; + } + + private function updateExtensions(): void + { + $db = Factory::getDbo(); + + try { + $query = $db->getQuery(true) + ->update('#__extensions') + ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) + ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + $db->setQuery($query)->execute(); + + $n = $db->getAffectedRows(); + if ($n > 0) { + $this->log("Bridge: updated {$n} row(s) in #__extensions."); + } + } catch (\Throwable $e) { + $this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error'); + } + } + + private function migrateStyles(): void + { + $db = Factory::getDbo(); + + // Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions) + $query = $db->getQuery(true) + ->select('*') + ->from('#__template_styles') + ->where('(' . $db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME) + . ' OR ' . $db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME) . ')') + ->where($db->quoteName('client_id') . ' = 0'); + $styles = $db->setQuery($query)->loadObjectList(); + + if (empty($styles)) { + $this->log('Bridge: no styles found to migrate.'); + return; + } + + foreach ($styles as $style) { + $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); + $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); + $newParams = is_string($style->params) + ? str_replace(self::OLD_NAME, self::NEW_NAME, $style->params) + : $style->params; + + $update = $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) + ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) + ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) + ->where('id = ' . (int) $style->id); + + try { + $db->setQuery($update)->execute(); + } catch (\Throwable $e) { + $this->log('Bridge: style update failed (id=' . $style->id . '): ' . $e->getMessage(), 'warning'); + } + } + + $this->log('Bridge: migrated ' . count($styles) . ' style(s).'); + } + + private function copyUserFiles(): void + { + $media = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + if (!is_dir($media)) { + return; + } + + // User files are already in the renamed media dir — nothing to copy. + // They were moved with the rename. Log for clarity. + $this->log('Bridge: user files preserved via directory rename.'); + } + + private function updateUpdateServer(): void + { + $db = Factory::getDbo(); + + try { + $query = $db->getQuery(true) + ->update('#__update_sites') + ->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL)) + ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY)) + ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); + $db->setQuery($query)->execute(); + + $n = $db->getAffectedRows(); + if ($n > 0) { + $this->log("Bridge: redirected {$n} update site(s) to MokoOnyx."); + } + } catch (\Throwable $e) { + $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning'); + } + + // Clear cached updates for old element + try { + $query = $db->getQuery(true) + ->delete('#__updates') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); + $db->setQuery($query)->execute(); + } catch (\Throwable $e) { + // Not critical + } + } + + // ── Logging ──────────────────────────────────────────────────────── + + private function log(string $message, string $priority = 'info'): void + { + static $init = false; + if (!$init) { + Log::addLogger( + ['text_file' => 'mokocassiopeia_bridge.log.php'], + Log::ALL, + ['mokocassiopeia_bridge'] + ); + $init = true; + } + + $levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR]; + Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge'); } } diff --git a/updates.xml b/updates.xml index b4cc69c..54d1610 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -17,9 +17,8 @@ 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.01-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.17-dev.zip - 089563017322317f989c49f8260d6f84cf2b84235cad4584504b716b9c429e83 development Moko Consulting https://mokoconsulting.tech @@ -38,9 +37,8 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/mokoonyx-01.00.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.10.13.zip - 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 alpha Moko Consulting https://mokoconsulting.tech @@ -59,9 +57,8 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/mokoonyx-01.00.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.10.13.zip - 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 beta Moko Consulting https://mokoconsulting.tech @@ -80,9 +77,8 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.10.13.zip - 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 rc Moko Consulting https://mokoconsulting.tech @@ -101,9 +97,8 @@ 2026-04-19 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03 - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip - 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4 stable Moko Consulting https://mokoconsulting.tech From d8196ef06be435462e5ed8832476dd1f9eb58bd1 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 17:53:30 +0000 Subject: [PATCH 27/41] =?UTF-8?q?chore(version):=20bump=2003.10.17=20?= =?UTF-8?q?=E2=86=92=2003.10.18=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba74520..781d001 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.17 + VERSION: 03.10.18 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index c14bcf8..f939989 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.17 + 03.10.18 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 54d1610..b22ebca 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.17 + 03.10.18 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.17-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.18-dev.zip development Moko Consulting From 87c076f346a2e6a448a05f430ec42b2045023336 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:06:52 -0500 Subject: [PATCH 28/41] fix: add SHA-256 if missing in updates.xml, add placeholders to all channels Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 7 +++++-- updates.xml | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d3a669..fc99b83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -403,8 +403,11 @@ jobs: # Update creation date block = re.sub(r"[^<]*", f"{date}", block) - # Update SHA-256 - block = re.sub(r"[^<]*", f"{sha256}", block) + # Update or add SHA-256 + if "" in block: + block = re.sub(r"[^<]*", f"{sha256}", block) + else: + block = block.replace("
", f"\n {sha256}") # Update Gitea download URL gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" diff --git a/updates.xml b/updates.xml index b22ebca..a08e3b4 100644 --- a/updates.xml +++ b/updates.xml @@ -19,6 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.18-dev.zip + development Moko Consulting https://mokoconsulting.tech @@ -39,6 +40,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.10.13.zip + alpha Moko Consulting https://mokoconsulting.tech @@ -59,6 +61,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.10.13.zip + beta Moko Consulting https://mokoconsulting.tech @@ -79,6 +82,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.10.13.zip + rc Moko Consulting https://mokoconsulting.tech @@ -99,6 +103,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip + stable Moko Consulting https://mokoconsulting.tech From 7618d44f1ebd5e6abfdb08610cf014b86dbbb436 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:09:04 -0500 Subject: [PATCH 29/41] fix: set dev SHA-256 and bump VERSION header to 03.10.18 Co-Authored-By: Claude Opus 4.6 (1M context) --- updates.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/updates.xml b/updates.xml index a08e3b4..3ab4d3e 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.18-dev.zip - + 9a13590dfadf18203652f82645fc9aed5c3a806b77da92317202cd6eda4e0da8 development Moko Consulting https://mokoconsulting.tech From f879d5002de225f5b2cc6c6b34ec30147c06794e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:14:27 -0500 Subject: [PATCH 30/41] Bridge: copy instead of rename, register as new extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rename() fails because Joomla's installer locks the directory during postflight. New approach: - Copy templates/mokocassiopeia → templates/mokoonyx (recursive) - Copy media dir the same way - Register MokoOnyx as a new extension in #__extensions - Create matching MokoOnyx styles with copied params - Set MokoOnyx as default, redirect update server - Old mokocassiopeia dir stays (user uninstalls later) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/script.php | 290 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 189 insertions(+), 101 deletions(-) diff --git a/src/script.php b/src/script.php index b379812..0a95e73 100644 --- a/src/script.php +++ b/src/script.php @@ -10,9 +10,10 @@ /** * MokoCassiopeia install/update/uninstall script. * - * On update: renames the template to MokoOnyx (dirs + database), - * copies all style params, creates matching styles, copies user files, - * and redirects the update server. No external downloads needed. + * On update: copies the template as MokoOnyx (new directory), updates the + * database to register MokoOnyx, migrates styles + params, and sets it as + * the default site template. The old MokoCassiopeia directory stays intact + * (Joomla's installer still needs it) — the user can uninstall it later. */ defined('_JEXEC') or die; @@ -33,7 +34,7 @@ class Tpl_MokocassiopeiaInstallerScript private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; - // ── Joomla lifecycle methods ─────────────────────────────────────── + // ── Joomla lifecycle ─────────────────────────────────────────────── public function preflight(string $type, InstallerAdapter $parent): bool { @@ -84,100 +85,165 @@ class Tpl_MokocassiopeiaInstallerScript return true; } - // ── Bridge: rename-in-place + DB migration ───────────────────────── + // ── Bridge ───────────────────────────────────────────────────────── private function bridge(): void { $app = Factory::getApplication(); - // 1. Rename template directory - $templateRenamed = $this->renameDir( - JPATH_ROOT . '/templates/' . self::OLD_NAME, - JPATH_ROOT . '/templates/' . self::NEW_NAME, - 'template' - ); - - if (!$templateRenamed && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { + // 1. Copy template directory (don't rename — Joomla still needs the old one) + $copied = $this->copyTemplateDir(); + if (!$copied && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) { $app->enqueueMessage( - 'Could not rename template directory to MokoOnyx. ' - . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.', + 'MokoOnyx bridge: could not create template directory. ' + . 'Please copy templates/mokocassiopeia to templates/mokoonyx manually.', 'warning' ); return; } - // 2. Rename media directory - $this->renameDir( - JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME, - JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME, - 'media' - ); + // 2. Copy media directory + $this->copyMediaDir(); - // 3. Update #__extensions - $this->updateExtensions(); + // 3. Register MokoOnyx in #__extensions (if not already there) + $this->registerExtension(); - // 4. Migrate template styles (create matching MokoOnyx styles with same params) + // 4. Migrate template styles (create MokoOnyx styles with same params) $this->migrateStyles(); - // 5. Copy user files (custom themes, user.css, user.js) - $this->copyUserFiles(); - - // 6. Redirect update server to MokoOnyx + // 5. Redirect update server to MokoOnyx $this->updateUpdateServer(); - // 7. Notify + // 6. Notify $app->enqueueMessage( - 'MokoCassiopeia has been renamed to MokoOnyx.
' - . 'All template settings, styles, and custom files have been migrated. ' - . 'MokoOnyx is now your active site template.', + 'MokoOnyx has been installed as a replacement for MokoCassiopeia.
' + . 'Your template settings have been migrated. MokoOnyx is now your active site template.
' + . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.', 'success' ); $this->log('=== Bridge completed ==='); } - // ── Bridge helpers ───────────────────────────────────────────────── + // ── Copy directories ─────────────────────────────────────────────── - private function renameDir(string $old, string $new, string $label): bool + private function copyTemplateDir(): bool { - if (!is_dir($old)) { - $this->log("Bridge: {$label} dir not found ({$old}) — skipping."); + $src = JPATH_ROOT . '/templates/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/templates/' . self::NEW_NAME; + + if (is_dir($dst)) { + $this->log('Bridge: templates/' . self::NEW_NAME . ' already exists — skipping copy.'); + return true; + } + + if (!is_dir($src)) { + $this->log('Bridge: source template dir not found.', 'error'); return false; } - if (is_dir($new)) { - $this->log("Bridge: {$label} dir already exists ({$new}) — skipping rename."); - return true; - } - - if (@rename($old, $new)) { - $this->log("Bridge: renamed {$label} dir → " . self::NEW_NAME); - return true; - } - - $this->log("Bridge: failed to rename {$label} dir.", 'error'); - return false; + $result = $this->recursiveCopy($src, $dst); + $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' template dir → ' . self::NEW_NAME); + return $result; } - private function updateExtensions(): void + private function copyMediaDir(): void + { + $src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME; + $dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + + if (is_dir($dst)) { + $this->log('Bridge: media dir already exists — skipping.'); + return; + } + + if (!is_dir($src)) { + $this->log('Bridge: source media dir not found — skipping.'); + return; + } + + $result = $this->recursiveCopy($src, $dst); + $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' media dir → ' . self::NEW_NAME); + } + + private function recursiveCopy(string $src, string $dst): bool + { + if (!mkdir($dst, 0755, true) && !is_dir($dst)) { + return false; + } + + $dir = opendir($src); + if ($dir === false) { + return false; + } + + while (($file = readdir($dir)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + + $srcPath = $src . '/' . $file; + $dstPath = $dst . '/' . $file; + + if (is_dir($srcPath)) { + $this->recursiveCopy($srcPath, $dstPath); + } else { + copy($srcPath, $dstPath); + } + } + + closedir($dir); + return true; + } + + // ── Database updates ─────────────────────────────────────────────── + + private function registerExtension(): void { $db = Factory::getDbo(); - try { - $query = $db->getQuery(true) - ->update('#__extensions') - ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) - ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - $db->setQuery($query)->execute(); + // Check if MokoOnyx is already registered + $query = $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + $exists = (int) $db->setQuery($query)->loadResult(); - $n = $db->getAffectedRows(); - if ($n > 0) { - $this->log("Bridge: updated {$n} row(s) in #__extensions."); - } + if ($exists) { + $this->log('Bridge: MokoOnyx already registered in #__extensions (id=' . $exists . ').'); + return; + } + + // Copy the MokoCassiopeia extension row and change element/name + $query = $db->getQuery(true) + ->select('*') + ->from('#__extensions') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + $oldExt = $db->setQuery($query)->loadObject(); + + if (!$oldExt) { + $this->log('Bridge: MokoCassiopeia not found in #__extensions.', 'warning'); + return; + } + + $newExt = clone $oldExt; + unset($newExt->extension_id); + $newExt->element = self::NEW_NAME; + $newExt->name = self::NEW_NAME; + + // Update manifest_cache to reflect new name + if (is_string($newExt->manifest_cache)) { + $newExt->manifest_cache = str_replace(self::OLD_NAME, self::NEW_NAME, $newExt->manifest_cache); + $newExt->manifest_cache = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $newExt->manifest_cache); + } + + try { + $db->insertObject('#__extensions', $newExt, 'extension_id'); + $this->log('Bridge: registered MokoOnyx in #__extensions (id=' . $newExt->extension_id . ').'); } catch (\Throwable $e) { - $this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error'); + $this->log('Bridge: failed to register extension: ' . $e->getMessage(), 'error'); } } @@ -185,54 +251,75 @@ class Tpl_MokocassiopeiaInstallerScript { $db = Factory::getDbo(); - // Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions) + // Get all MokoCassiopeia styles $query = $db->getQuery(true) ->select('*') ->from('#__template_styles') - ->where('(' . $db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME) - . ' OR ' . $db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME) . ')') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)) ->where($db->quoteName('client_id') . ' = 0'); - $styles = $db->setQuery($query)->loadObjectList(); + $oldStyles = $db->setQuery($query)->loadObjectList(); - if (empty($styles)) { - $this->log('Bridge: no styles found to migrate.'); + if (empty($oldStyles)) { + $this->log('Bridge: no MokoCassiopeia styles found.'); return; } - foreach ($styles as $style) { - $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title); - $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); - $newParams = is_string($style->params) - ? str_replace(self::OLD_NAME, self::NEW_NAME, $style->params) - : $style->params; + $this->log('Bridge: migrating ' . count($oldStyles) . ' style(s).'); - $update = $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) - ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) - ->where('id = ' . (int) $style->id); + foreach ($oldStyles as $old) { + $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $old->title); + $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); + + // Skip if MokoOnyx already has this style + $check = $db->getQuery(true) + ->select('COUNT(*)') + ->from('#__template_styles') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle)); + if ((int) $db->setQuery($check)->loadResult() > 0) { + $this->log("Bridge: style '{$newTitle}' already exists — skipping."); + continue; + } + + $newParams = is_string($old->params) + ? str_replace(self::OLD_NAME, self::NEW_NAME, $old->params) + : $old->params; + + $new = clone $old; + unset($new->id); + $new->template = self::NEW_NAME; + $new->title = $newTitle; + $new->params = $newParams; + $new->home = 0; try { - $db->setQuery($update)->execute(); + $db->insertObject('#__template_styles', $new, 'id'); + $newId = $new->id; + + // If old was default, make new default + if ($old->home == 1) { + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 1') + ->where('id = ' . (int) $newId) + )->execute(); + + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 0') + ->where('id = ' . (int) $old->id) + )->execute(); + + $this->log('Bridge: set MokoOnyx as default site template.'); + } + + $this->log("Bridge: created style '{$newTitle}'."); } catch (\Throwable $e) { - $this->log('Bridge: style update failed (id=' . $style->id . '): ' . $e->getMessage(), 'warning'); + $this->log("Bridge: failed to create style '{$newTitle}': " . $e->getMessage(), 'warning'); } } - - $this->log('Bridge: migrated ' . count($styles) . ' style(s).'); - } - - private function copyUserFiles(): void - { - $media = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; - if (!is_dir($media)) { - return; - } - - // User files are already in the renamed media dir — nothing to copy. - // They were moved with the rename. Log for clarity. - $this->log('Bridge: user files preserved via directory rename.'); } private function updateUpdateServer(): void @@ -255,12 +342,13 @@ class Tpl_MokocassiopeiaInstallerScript $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning'); } - // Clear cached updates for old element + // Clear cached updates try { - $query = $db->getQuery(true) - ->delete('#__updates') - ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)); - $db->setQuery($query)->execute(); + $db->setQuery( + $db->getQuery(true) + ->delete('#__updates') + ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME)) + )->execute(); } catch (\Throwable $e) { // Not critical } From 97099a44783cdbd682eaf3dd425af9f3cc27305a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:15:11 +0000 Subject: [PATCH 31/41] =?UTF-8?q?chore(version):=20bump=2003.10.18=20?= =?UTF-8?q?=E2=86=92=2003.10.19=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 781d001..563d954 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.18 + VERSION: 03.10.19 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index f939989..7d74bad 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.18 + 03.10.19 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 3ab4d3e..e195436 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.18 + 03.10.19 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.18-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.19-dev.zip 9a13590dfadf18203652f82645fc9aed5c3a806b77da92317202cd6eda4e0da8 development From 9adc037f3e7041e92ac9f32efbfbd6996e3a1d43 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:15:15 +0000 Subject: [PATCH 32/41] chore: update development SHA-256 for 03.10.19 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index e195436..195bdc0 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.19-dev.zip - 9a13590dfadf18203652f82645fc9aed5c3a806b77da92317202cd6eda4e0da8 + ade676d2ccff731baee637d9fc4e78e2612c03a210bb184e51c4df1386983ace development Moko Consulting https://mokoconsulting.tech From 2675f6cef7a0f54d9fbad87470f8f55af27d56be Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:22:42 -0500 Subject: [PATCH 33/41] fix: implement InstallerScriptInterface for Joomla 6 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script wasn't running — Joomla 6 requires InstallerScriptInterface. Also added debug file_put_contents to confirm execution. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/script.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/script.php b/src/script.php index 0a95e73..c6d0023 100644 --- a/src/script.php +++ b/src/script.php @@ -20,9 +20,10 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Installer\InstallerAdapter; +use Joomla\CMS\Installer\InstallerScriptInterface; use Joomla\CMS\Log\Log; -class Tpl_MokocassiopeiaInstallerScript +class Tpl_MokocassiopeiaInstallerScript implements InstallerScriptInterface { private const MIN_PHP = '8.1.0'; private const MIN_JOOMLA = '4.4.0'; @@ -77,6 +78,13 @@ class Tpl_MokocassiopeiaInstallerScript public function postflight(string $type, InstallerAdapter $parent): bool { + // Debug: write directly to confirm script runs + @file_put_contents( + JPATH_ROOT . '/administrator/logs/bridge_debug.txt', + date('Y-m-d H:i:s') . " postflight called, type={$type}\n", + FILE_APPEND + ); + if ($type === 'update') { $this->log('=== MokoCassiopeia → MokoOnyx bridge ==='); $this->bridge(); From b3fd76adecfaa2d287a5470cf357162663d6d87c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:26:39 -0500 Subject: [PATCH 34/41] =?UTF-8?q?Update=20manifest=20description=20?= =?UTF-8?q?=E2=80=94=20migration=20notice=20for=20MokoOnyx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/templateDetails.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 7d74bad..959f885 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -45,7 +45,7 @@ Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting - Version 03.10.13 License Joomla PHP

MokoCassiopeia Template Description

MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

]]> +

⚠️ MokoCassiopeia → MokoOnyx Migration

This template is being renamed to MokoOnyx. Updating to this version will automatically:

  • Create the new templates/mokoonyx directory
  • Copy your template settings and custom styles to MokoOnyx
  • Set MokoOnyx as your default site template
  • Redirect future updates to the MokoOnyx repository
After updating, you can safely uninstall MokoCassiopeia from Extensions → Manage.

License Joomla PHP

MokoCassiopeia is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration.

This template is now succeeded by MokoOnyx — same features, new name. All future development continues under the MokoOnyx project.

]]>
1 component.php From 2ed4c1f37f0792ba15c6e1b1b3a49c3fec1c4aa3 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:41:08 +0000 Subject: [PATCH 35/41] =?UTF-8?q?chore(version):=20bump=2003.10.19=20?= =?UTF-8?q?=E2=86=92=2003.10.20=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 563d954..af6974c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.19 + VERSION: 03.10.20 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 959f885..ee29bb3 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.19 + 03.10.20 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 195bdc0..9430d4b 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.19 + 03.10.20 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.19-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.20-dev.zip ade676d2ccff731baee637d9fc4e78e2612c03a210bb184e51c4df1386983ace development From 12f17d706f18aa155692914db90e56387e9bb99a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:41:12 +0000 Subject: [PATCH 36/41] chore: update development SHA-256 for 03.10.20 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 9430d4b..9a61584 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.20-dev.zip - ade676d2ccff731baee637d9fc4e78e2612c03a210bb184e51c4df1386983ace + afc0739299bab1316aa633a935aae3186521cf1fc8ece346dc5b17abf874481d development Moko Consulting https://mokoconsulting.tech From 6fa86671c643983bb28d2ea4455966259add7284 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 13:45:43 -0500 Subject: [PATCH 37/41] Update description: manual MokoOnyx install link + migration steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge script not firing in Joomla 6 — provide direct download link and step-by-step instructions instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/templateDetails.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templateDetails.xml b/src/templateDetails.xml index ee29bb3..83b7443 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -45,7 +45,7 @@ Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting -

⚠️ MokoCassiopeia → MokoOnyx Migration

This template is being renamed to MokoOnyx. Updating to this version will automatically:

  • Create the new templates/mokoonyx directory
  • Copy your template settings and custom styles to MokoOnyx
  • Set MokoOnyx as your default site template
  • Redirect future updates to the MokoOnyx repository
After updating, you can safely uninstall MokoCassiopeia from Extensions → Manage.

License Joomla PHP

MokoCassiopeia is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration.

This template is now succeeded by MokoOnyx — same features, new name. All future development continues under the MokoOnyx project.

]]>
+

⚠️ MokoCassiopeia has been renamed to MokoOnyx

This template is no longer maintained. Please install MokoOnyx manually to continue receiving updates. MokoOnyx has the same features and will automatically import your MokoCassiopeia settings on first install.

Steps to migrate:

  1. Download MokoOnyx: mokoonyx-01.00.00.zip
  2. Install via System → Install → Extensions → Upload Package File
  3. MokoOnyx will automatically copy your settings and set itself as the default template
  4. Uninstall MokoCassiopeia from Extensions → Manage

Download MokoOnyx

License Joomla PHP

MokoCassiopeia is succeeded by MokoOnyx — same features, new name. All future development continues under the MokoOnyx project.

]]>
1 component.php From e6dd4ca84f52f8c3f1bb983f86a90bdd45478c67 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:47:18 +0000 Subject: [PATCH 38/41] =?UTF-8?q?chore(version):=20bump=2003.10.20=20?= =?UTF-8?q?=E2=86=92=2003.10.21=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/templateDetails.xml | 2 +- updates.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index af6974c..f9f1710 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ INGROUP: MokoCassiopeia.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia FILE: ./README.md - VERSION: 03.10.20 + VERSION: 03.10.21 BRIEF: Documentation for MokoCassiopeia template --> diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 83b7443..e1ae6c1 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,7 +39,7 @@ MokoCassiopeia - 03.10.20 + 03.10.21 script.php 2026-04-19 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 9a61584..63b99a2 100644 --- a/updates.xml +++ b/updates.xml @@ -13,11 +13,11 @@ mokocassiopeia template site - 03.10.20 + 03.10.21 2026-04-21 https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.20-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.21-dev.zip afc0739299bab1316aa633a935aae3186521cf1fc8ece346dc5b17abf874481d development From 3a41f13e8e815f78c0e9a7ccdfef008084683c98 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 21 Apr 2026 18:47:21 +0000 Subject: [PATCH 39/41] chore: update development SHA-256 for 03.10.21 [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 63b99a2..e24e562 100644 --- a/updates.xml +++ b/updates.xml @@ -19,7 +19,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.21-dev.zip - afc0739299bab1316aa633a935aae3186521cf1fc8ece346dc5b17abf874481d + 2d21714719dd3e3d87228e1d021d5fc69a96a837a9ec2d5880da733eb28fa5d0 development Moko Consulting https://mokoconsulting.tech From fb2c9b6f458a231e3579309b4a8c3aaa0c141b10 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 14:32:42 -0500 Subject: [PATCH 40/41] docs: redirect roadmap to MokoOnyx, link to stable tag instead of direct download Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/ROADMAP.md | 926 +--------------------------------------- src/templateDetails.xml | 2 +- 2 files changed, 21 insertions(+), 907 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c47caec..761582f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,917 +1,31 @@ -# MokoCassiopeia Roadmap (VERSION: 03.09.03) +# MokoCassiopeia Roadmap -This document provides a comprehensive, version-specific roadmap for the MokoCassiopeia Joomla template, tracking feature evolution, current capabilities, and planned enhancements. +**MokoCassiopeia has been renamed to MokoOnyx.** All future development continues under the MokoOnyx project. -## Table of Contents +See the [MokoOnyx Roadmap](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/src/branch/dev/docs/ROADMAP.md) for all planned features and releases. -- [Version Timeline](#version-timeline) - - [Past Releases](#past-releases) - - [Future Roadmap (5-Year Plan)](#future-roadmap-5-year-plan) -- [Current Release (v03.06.03)](#current-release-v030603) -- [Implemented Features](#implemented-features) -- [Planned Features](#planned-features) -- [Development Priorities](#development-priorities) -- [Long-term Vision](#long-term-vision) -- [External Resources](#external-resources) +## Migration ---- +To migrate from MokoCassiopeia to MokoOnyx: -## Version Timeline - -### Past Releases - -### v03.05.01 (2026-01-09) - Standards & Security -**Status**: Released (CHANGELOG entry exists, code files pending version update) - -**Added**: -- Dependency review workflow for vulnerability scanning -- Standards compliance workflow for MokoStandards validation -- Dependabot configuration for automated security updates -- Documentation index (`docs/README.md`) - -**Changed**: -- Removed custom CodeQL workflow (using GitHub's default setup) -- Enforced repository compliance with MokoStandards -- Improved security posture with automated scanning - -### v03.06.00 (2026-01-28) - Version Update -**Status**: Current Release (in code) - -**Changed**: -- Updated version to 03.06.00 across all files - -### v03.05.00 (2026-01-04) - Workflow & Governance -**Status**: Mentioned in CHANGELOG (v03.05.00) - -**Added**: -- `.github/workflows` directory structure -- CODE_OF_CONDUCT.md from MokoStandards -- CONTRIBUTING.md from MokoStandards - -**Changed**: -- TODO items to be split to separate file (tracked) - -### v03.01.00 (2025-12-16) - CI/CD Foundation -**Added**: -- Initial GitHub Actions workflows - -### v03.00.00 (2025-12-09) - Font Awesome 7 Upgrade -**Updated**: -- Copyright headers to MokoCodingDefaults standards -- Fixed color style injection in `index.php` -- Upgraded Font Awesome 6 to Font Awesome 7 Free -- Added Font Awesome 7 Free style fallback - -**Removed**: -- Deprecated CODE_OF_CONDUCT.md -- Deprecated CONTRIBUTING.md - -### v02.01.05 (2025-09-04) - CSS Refinement -**Fixed**: -- Removed vmbasic.css -- Repaired template.css and colors_standard.css - -### v02.00.00 (2025-08-30) - Dark Mode & TOC -**Major Features**: -- **Dark Mode Toggle System** - - Frontend toggle switch with localStorage persistence - - Admin-configurable default mode - - CSS rules for light/dark themes - - JavaScript-powered mode switching - -- **Enhanced Template Parameters** - - Logo parameter support - - GTM container ID configuration - - Dark mode defaults in settings - - Updated metadata and copyright headers - -- **Expanded Table of Contents** - - Automatic TOC injection - - User-selectable placement (`toc-left` or `toc-right`) - - Article options integration - -**Improvements**: -- Cleaned up `index.php` (removed duplicate skip-to-content calls) -- Consolidated JavaScript asset loading -- Streamlined CSS for toggle switch -- Accessibility refinements (typography, color contrast) -- Fixed missing logo parameter in header -- Corrected stylesheet inconsistencies -- Patched redundant script includes - -### v01.00.00 - Initial Public Release -**Core Features**: -- Font Awesome 6 integration -- Bootstrap 5 helpers and utilities -- Automatic Table of Contents (TOC) utility -- Moko Expansions: Google Tag Manager / GA4 hooks -- Built on Joomla's Cassiopeia template - ---- - -### Future Roadmap (5-Year Plan) - -The following versions represent our planned annual major releases, each building upon the previous version's foundation. - -#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance -**Status**: Planned -**Target Release**: December 2027 - -**Major Template Features**: -- **WCAG 2.1 AA Compliance** - - Full accessibility audit and remediation - - High-contrast theme options - - Screen reader optimizations - - Keyboard navigation enhancements - - ARIA landmark improvements - - Skip navigation enhancements - -- **Template Performance Optimizations** - - Critical CSS inlining for faster first paint - - Lazy loading for images and below-fold content - - WebP image support with automatic fallbacks - - Advanced asset bundling and minification - - Template asset caching (CSS/JS bundles) - -- **Enhanced Layout System** - - Additional responsive grid layouts - - Flexible module position system - - Column layout presets (2-col, 3-col, 4-col variations) - - Grid/masonry article layouts - - Sticky sidebar options - -- **Typography Enhancements** - - Advanced typography controls in template settings - - Additional font pairing presets - - Custom font upload support - - Line height and letter spacing controls - - Responsive typography scaling - -- **Developer Experience** - - Development mode enablement (unminified assets, debug output) - - Live reload during development - - Enhanced error logging and diagnostics - - Template debugging tools - - Style guide generator - -- **Content Display Features** - - Soft offline mode (category-based access during maintenance) - - Enhanced article layouts (grid, masonry, timeline) - - Image caption styling options - - Quote block styling variations - - Enhanced breadcrumb customization - -**Template Infrastructure**: -- Expanded template parameter validation -- Enhanced template override detection -- Automated template compatibility testing -- Template performance profiling tools - ---- - -#### v05.00.00 (Q4 2028) - Advanced Layouts & Template Customization -**Status**: Planned -**Target Release**: December 2028 - -**Major Template Features**: -- **Enhanced Layout Builder** - - Template-based page layout variations - - Configurable layout options via template parameters - - Layout presets library (blog, portfolio, business, magazine) - - Module position layout manager - - Visual layout preview in admin - -- **Advanced Styling System** - - Extended color palette management (unlimited custom palettes) - - CSS variable editor in template settings - - Style presets for different site types - - Border radius and spacing controls - - Box shadow and effect controls - -- **Template Component Enhancements** - - Enhanced menu styling options (mega menu support) - - Advanced header variations (transparent, sticky, minimal) - - Footer layout options (column variations, widgets) - - Sidebar styling and behavior options - - Hero section templates and variations - -- **Content Display Options** - - Article intro/full text display controls - - Category layout variations (grid, list, masonry, cards) - - Featured content sections - - Related articles display options - - Author bio box styling - -- **Responsive Design Improvements** - - Mobile-first navigation patterns - - Tablet-specific layout controls - - Responsive image sizing options - - Mobile header variations - - Touch-friendly interface elements - -- **Template Integration Features** - - Enhanced VirtueMart template overrides - - Contact form styling variations - - Search result layout options - - Error page customization - - Archive page templates - -**Template Infrastructure**: -- Joomla 6.x template compatibility (if released) -- PHP 8.2+ support -- Template child theme support -- Template preset import/export functionality - ---- - -#### v06.00.00 (Q4 2029) - Template Extensions & Advanced Features -**Status**: Planned -**Target Release**: December 2029 - -**Major Template Features**: -- **Template Marketplace & Extensions** - - Template addon system for modular features - - Community-contributed template extensions - - Template preset marketplace - - Style pack distribution system - - Template component library - -- **Advanced Module System** - - Custom module chrome options - - Module animation effects - - Module visibility controls (scroll, time-based) - - Module group management - - Module style inheritance - -- **Enhanced Media Handling** - - Background image options per page/section - - Image overlay controls - - Parallax scrolling effects - - Video background support - - Gallery template variations - -- **Template Branding Options** - - Multiple logo upload (standard, retina, mobile) - - Favicon and app icon management - - Custom loading screen/animations - - Watermark options - - Brand color scheme generator - -- **Advanced Header/Footer** - - Multiple header layout presets - - Sticky header variations and behaviors - - Header transparency controls - - Footer widget areas expansion - - Floating action buttons - -- **Content Enhancement Features** - - Reading progress indicator - - Social sharing buttons (template-integrated) - - Print-friendly styles - - Reading time estimation display - - Content table enhancements - -- **Template SEO Features** - - Schema markup templates for common types - - Open Graph tag management - - Twitter Card support - - Breadcrumb schema integration - - Meta tag template controls - -**Template Infrastructure**: -- Template versioning system -- Template backup/restore functionality -- Template A/B testing support -- Multi-language template variations -- Template documentation generator - ---- - -#### v07.00.00 (Q4 2030) - Modern Template Standards & Enhancements -**Status**: Planned -**Target Release**: December 2030 - -**Major Template Features**: -- **Modern CSS Features** - - CSS Grid layout system integration - - CSS Container Queries support - - CSS Cascade Layers implementation (layered style priority system) - - Custom properties (CSS variables) UI - - Modern filter and backdrop effects - -- **Progressive Template Features** - - Offline-capable template assets - - Service worker template integration - - App manifest generation - - Install to home screen support - - Template asset preloading strategies - -- **Animation & Interaction** - - Scroll-triggered animations - - Hover effect library - - Page transition effects - - Micro-interactions for UI elements - - Loading animation options - -- **Advanced Responsive Features** - - Container-based responsive design - - Element visibility by viewport - - Responsive navigation patterns library - - Mobile-optimized interactions - - Adaptive image loading - -- **Template Accessibility Features** - - Focus indicators customization - - Reduced motion preferences support - - High contrast mode automation - - Keyboard navigation patterns - - ARIA live regions for dynamic content - -- **Content Presentation** - - Advanced blockquote styles - - Code snippet highlighting themes - - Table styling variations - - List styling options - - Custom content block templates - -- **Template Performance** - - Resource hints (preconnect, prefetch) - - Optimal asset delivery strategies - - Image format optimization (AVIF support) - - Font loading optimization - - Template metrics dashboard - -**Template Infrastructure**: -- Template pattern library -- Design token system -- Template component documentation -- Automated template testing suite -- Template performance monitoring - ---- - -#### v08.00.00 (Q4 2031) - Next-Generation Template Features -**Status**: Conceptual -**Target Release**: December 2031 - -**Major Template Features**: -- **Advanced Layout Systems** - - Subgrid support for complex layouts - - Multi-column layout variations - - Asymmetric grid systems - - Dynamic layout switching - - Layout constraint system - -- **Enhanced Visual Customization** - - Real-time style editor - - Template style variations manager - - Custom CSS injection with validation - - Style inheritance and override system - - Visual design tokens editor - -- **Template Component Library** - - Comprehensive UI component set - - Reusable template blocks - - Component variation system - - Template snippet library - - Pattern library integration - -- **Advanced Typography System** - - Variable font support - - Advanced typographic scales - - Font pairing recommendations - - Fluid typography system - - Custom font fallback chains - -- **Template Integration Features** - - Enhanced component overrides - - Template hooks system - - Event-based template modifications - - Custom field rendering templates - - Module position API enhancements - -- **Responsive & Adaptive Design** - - Advanced breakpoint management - - Element-specific responsive controls - - Adaptive images with art direction - - Responsive typography system - - Context-aware component rendering - -- **Template Ecosystem** - - Child template framework - - Template derivative system - - Community template marketplace - - Template rating and review system - - Professional template support network - -- **Template Quality & Maintenance** - - Automated accessibility testing - - Template performance auditing - - Code quality monitoring - - Update notification system - - Template health dashboard - -**Template Infrastructure**: -- Template API for extensibility -- Template package manager -- Template development CLI tools -- Template migration utilities -- Comprehensive template documentation system - ---- - -## Current Release (v03.06.03) - -### System Requirements -- **Joomla**: 4.4.x or 5.x -- **PHP**: 8.0+ -- **Database**: MySQL/MariaDB compatible - -### Architecture -- **Base Template**: Joomla Cassiopeia -- **Enhancement Layer**: Non-invasive overrides -- **Asset Management**: Joomla Web Asset Manager (WAM) -- **Frontend Framework**: Bootstrap 5 -- **Icon Library**: Font Awesome 7 Free - ---- - -## Implemented Features - -### 🎨 Theming & Visual Design - -#### Color Palette System -- **3 Built-in Palettes**: Standard, Alternative, Custom -- **Dual Mode Support**: Separate light and dark configurations -- **Custom Palettes**: User-definable via `colors_custom.css` -- **Location**: `src/media/css/colors/{light|dark}/` - -#### Dark Mode System -- **Toggle Controls**: Switch (Light↔Dark) or Radios (Light/Dark/System) -- **Default Mode**: Admin-configurable (system, light, or dark) -- **Persistence**: localStorage for user preferences -- **Auto-Detection**: Optional system preference detection -- **Meta Tags**: `color-scheme` and `theme-color` support -- **ARIA Bridge**: Bootstrap ARIA compatibility - -#### Typography -- **Font Schemes**: - - Local: Roboto - - Web (Google Fonts): Fira Sans, Roboto + Noto Sans -- **Admin-Configurable**: Template settings dropdown - -#### Branding -- **Logo Support**: Custom logo upload -- **Site Title**: Text-based branding option -- **Site Description**: Tagline/subtitle field -- **Font Awesome Kit**: Optional custom kit integration - -### 📐 Layout & Structure - -#### Module Positions (23 Total) -**Header Area**: -- topbar, below-topbar, below-logo, menu, search, banner - -**Content Area**: -- top-a, top-b, main-top, main-bottom, breadcrumbs -- sidebar-left, sidebar-right - -**Footer Area**: -- bottom-a, bottom-b, footer-menu, footer - -**Special**: -- debug, offline-header, offline, offline-footer -- drawer-left, drawer-right - -#### Layout Options -- **Container Type**: Fluid or Static -- **Sticky Header**: Optional fixed navigation -- **Back-to-Top Button**: Scrollable page support - -### 📝 Content Features - -#### Table of Contents (TOC) -- **Automatic Generation**: From article headings -- **Placement Options**: `toc-left` or `toc-right` layouts -- **Article Integration**: Via Options → Layout dropdown -- **Responsive**: Mobile-friendly sidebar placement - -#### Article Layouts -- **Default**: Standard Cassiopeia layout -- **TOC Variants**: Left-sidebar or right-sidebar TOC -- **Custom Overrides**: Located in `html/com_content/article/` - -### 📊 Analytics & Tracking - -#### Google Tag Manager (GTM) -- **Enable/Disable**: Admin toggle -- **Container ID**: Template parameter field -- **Implementation**: Head and body script injection -- **GDPR-Ready**: Configurable consent defaults - -#### Google Analytics 4 (GA4) -- **Enable/Disable**: Admin toggle -- **Property ID**: Template parameter field -- **Universal Analytics Fallback**: Legacy UA support -- **Privacy-First**: Conditional loading based on settings - -### 🎛️ Customization & Developer Tools - -#### Custom Code Injection -- **Head Start**: Custom HTML/JS before `` -- **Head End**: Custom HTML/JS at end of `` -- **Raw HTML**: Unfiltered code injection for advanced users - -#### Drawer System -- **Left/Right Drawers**: Offcanvas menu areas -- **Icon Customization**: Font Awesome icon selection -- **Default Icons**: - - Left: `fa-solid fa-chevron-right` - - Right: `fa-solid fa-chevron-left` - -#### Asset Management -- **Joomla WAM**: Complete asset registry in `joomla.asset.json` -- **Development/Production Modes**: Minified and unminified assets -- **Dependency Management**: Automatic script/style loading - -### 🏗️ Template Overrides - -#### Component Overrides -**Content (com_content)**: -- Article layouts (default, toc-left, toc-right) -- Category layouts (blog, list) -- Featured articles - -**Contact (com_contact)**: -- Contact form layouts - -**Engage (com_engage)**: -- Comment system integration - -#### Module Overrides -**Menu (mod_menu)**: -- Metis dropdown menu -- Offcanvas navigation - -**VirtueMart**: -- Product display (`mod_virtuemart_product`) -- Shopping cart (`mod_virtuemart_cart`) -- Manufacturer display (`mod_virtuemart_manufacturer`) -- Category display (`mod_virtuemart_category`) -- Currency selector (`mod_virtuemart_currencies`) - -**Other Modules**: -- Custom HTML (`mod_custom`) -- GABble social integration (`mod_gabble`) - -**Membership System (OS Membership)**: -- Plan layouts (default, pricing tables) -- Member management interfaces - -### 🔧 Configuration Parameters - -#### Theme Tab -**General**: -- `theme_enabled` - Enable/disable theme system -- `theme_control_type` - Toggle UI type (switch/radios/none) -- `theme_default_choice` - Default mode (system/light/dark) -- `theme_auto_dark` - Auto-detect system preference -- `theme_meta_color_scheme` - Inject `color-scheme` meta tag -- `theme_meta_theme_color` - Inject `theme-color` meta tag -- `theme_bridge_bs_aria` - Bootstrap ARIA compatibility - -**Variables & Palettes**: -- `colorLightName` - Light mode color scheme -- `colorDarkName` - Dark mode color scheme - -**Typography**: -- `useFontScheme` - Font selection (local/web) - -**Branding & Icons**: -- `brand` - Show/hide branding -- `logoFile` - Logo upload path -- `siteTitle` - Site title text -- `siteDescription` - Site tagline -- `fA6KitCode` - Font Awesome kit code - -**Header & Navigation**: -- `stickyHeader` - Fixed navigation -- `backTop` - Back-to-top button - -**Toggle UI**: -- `theme_fab_enabled` - Floating action button for theme toggle -- `theme_fab_pos` - FAB position (br/bl/tr/tl) - -#### Google Tab -- `googletagmanager` - Enable GTM -- `googletagmanagerid` - GTM container ID -- `googleanalytics` - Enable GA4 -- `googleanalyticsid` - GA4 property ID - -#### Custom Code Tab -- `custom_head_start` - Custom code at head start -- `custom_head_end` - Custom code at head end - -#### Drawers Tab -- `drawerLeftIcon` - Left drawer icon (Font Awesome class) -- `drawerRightIcon` - Right drawer icon (Font Awesome class) - -#### Advanced Tab -- `fluidContainer` - Container layout (static/fluid) - -### 🛠️ Development Tools - -#### Quality Assurance -- **Codeception**: Automated testing framework -- **PHPStan**: Static analysis (level 8+) -- **PHPCS**: Code style validation (PSR-12) -- **PHPCompatibility**: PHP 8.0+ compatibility checks - -#### CI/CD Workflows -- **Dependency Review**: Vulnerability scanning -- **Standards Compliance**: MokoStandards validation -- **CodeQL**: Security analysis (GitHub default) -- **Dependabot**: Automated dependency updates - -#### Documentation -- **Quick Start**: 5-minute developer setup -- **Workflow Guide**: Git strategy, branching, releases -- **Joomla Development**: Testing, packaging, multi-version support - ---- - -## Planned Features - -### 🚧 In Development - -#### Soft Offline Mode (v03.07.00 - Planned) -**Status**: Planned for v03.07.00 -**Priority**: High -**Description**: Keep selected categories accessible during site maintenance mode with persistent links to essential pages - -**Use Cases**: -- Legal documents remain viewable during downtime -- Policy pages accessible for compliance requirements -- Terms of service always available to users -- Privacy policy accessible at all times -- Essential public information during maintenance - -**Technical Specifications**: -- **Configuration Method**: Template parameters in `templateDetails.xml` -- **Category Access**: Category IDs stored as comma-separated values -- **Persistent Links**: Direct article/menu item links always visible -- **Access Control**: Check in `offline.php` template file -- **Content Rendering**: Use Joomla's content component to fetch articles -- **Security**: Maintain proper access levels and permissions - -**Implementation Plan**: -1. Add category selection field to template parameters -2. Add persistent link configuration (Terms of Service, Privacy Policy, etc.) -3. Modify `offline.php` to check for allowed categories -4. Add persistent link display in offline mode header/footer -5. Implement category content fetching during offline mode -6. Add styling for offline mode category display and persistent links -7. Test with various category and link configurations -8. Document admin configuration steps - -**Configuration Interface**: -- **Category Field Type**: Category multiselect in template settings - - **Label**: "Categories Accessible During Offline Mode" - - **Default**: None (all content hidden by default) -- **Persistent Links**: Text fields for essential always-available links - - **Terms of Service URL**: Direct link to TOS article/page - - **Privacy Policy URL**: Direct link to privacy policy - - **Contact URL**: Optional contact page link - - **Custom Link 1-3**: Additional persistent links if needed -- **Admin Path**: System → Site Templates → MokoCassiopeia → Advanced → Offline Mode Settings - -**Persistent Links Feature**: -- **Display Location**: Footer of offline page -- **Styling**: Clearly visible, accessible links -- **Format**: "Terms of Service | Privacy Policy | Contact" -- **Behavior**: Links bypass offline mode restrictions -- **Validation**: Check if URLs are valid Joomla routes - -**Benefits**: -- ✅ Compliance: Keep legal pages accessible -- ✅ Transparency: Users can access essential information -- ✅ Flexibility: Admin control over which categories remain visible -- ✅ Security: Respects Joomla access levels -- ✅ Legal Protection: Terms of Service always accessible -- ✅ User Trust: Privacy policy always available - -**Milestone**: Target release v03.07.00 (Q2 2026) - -#### TODO Tracking System -**Status**: Mentioned in CHANGELOG (v03.05.00) -**Description**: Separate TODO tracking file -**Purpose**: Centralized issue and feature tracking outside changelog - -### 🔮 Future Enhancements - -#### Development Mode (Commented Out) -**Status**: Code exists but disabled -**Location**: `templateDetails.xml` line 91 -**Description**: Comprehensive development mode toggle -**Potential Features**: -- Unminified asset loading -- Debug output -- Performance profiling -- Template cache bypass - -#### Potential Features (Community Requested) -*Note: These are conceptual and not yet officially planned* - -**Enhanced Accessibility**: -- WCAG 2.1 AAA compliance mode -- High-contrast themes -- Screen reader optimizations -- Keyboard navigation improvements - -**Template Layout Features**: -- Advanced responsive grid layouts -- Multiple column variations -- Custom module position system -- Layout preset library - -**Template Styling Features**: -- Extended color palette management -- Custom font upload support -- Typography scale controls -- Visual style editor - ---- - -## Development Priorities - -### Immediate Focus (v03.x - 2026) -1. **Bootstrap TOC Integration**: Complete and document v1.0.1 implementation ✅ -2. **Soft Offline Mode**: Implement category-based offline access (Target: v03.07.00) -3. **TODO Tracking System**: Implement separate file for issue tracking -4. **Security Updates**: Maintain Dependabot and CodeQL scans -5. **Documentation**: Keep docs synchronized with features -6. **Bug Fixes**: Address reported issues and edge cases - -### v04.00.00 Priorities (2027) - Template Foundation -1. **WCAG 2.1 AA Compliance**: Full template accessibility audit and implementation -2. **Template Performance**: Critical CSS, lazy loading, WebP support -3. **Layout System**: Enhanced responsive grid and module positions -4. **Development Mode**: Enable comprehensive template developer tools - -### v05.00.00 Priorities (2028) - Template Customization -1. **Layout Builder**: Template-based page layout system -2. **Styling System**: Extended color palettes and CSS variable management -3. **Template Components**: Enhanced header, footer, and menu variations -4. **Responsive Design**: Mobile-first navigation and layout improvements - -### v06.00.00 Priorities (2029) - Template Extensions -1. **Template Marketplace**: Addon system and community extensions -2. **Module System**: Advanced module chrome and animation options -3. **Media Handling**: Background images, parallax, video backgrounds -4. **Template SEO**: Schema markup templates and meta tag controls - -### v07.00.00+ Priorities (2030+) - Modern Standards -1. **Modern CSS**: Grid, Container Queries, Cascade Layers -2. **Progressive Template**: Offline-capable assets and PWA features -3. **Animation System**: Scroll-triggered effects and micro-interactions -4. **Template Performance**: Advanced optimization and monitoring - ---- - -## Long-term Vision - -### Mission Statement -MokoCassiopeia aims to be the **most developer-friendly, user-customizable, and standards-compliant Joomla template** while maintaining minimal core overrides for maximum upgrade compatibility. - -### Core Principles -1. **Non-Invasive**: Minimal Cassiopeia overrides -2. **Standards-First**: MokoStandards compliance -3. **Accessibility**: WCAG 2.1 compliance -4. **Performance**: Fast, optimized delivery -5. **Developer Experience**: Clear docs, easy setup, powerful tools -6. **Template-Focused**: Pure template features without complex external dependencies - -### 5-Year Strategic Roadmap (Template Features) - -#### 2027 (v04.00.00) - Accessibility & Performance -- Achieve WCAG 2.1 AA compliance for all template elements -- Implement critical template performance optimizations -- Enhance template layout system with flexible grids -- Enable comprehensive development mode for template developers - -#### 2028 (v05.00.00) - Layouts & Customization -- Launch template-based layout builder system -- Deploy extended styling and customization options -- Enhance template component variations (headers, footers, menus) -- Improve responsive design patterns for all devices - -#### 2029 (v06.00.00) - Extensions & Enhancements -- Introduce template addon and extension system -- Launch template preset marketplace -- Deploy advanced module styling and animation features -- Implement comprehensive template SEO controls - -#### 2030 (v07.00.00) - Modern Standards -- Adopt modern CSS standards (Grid, Container Queries, Cascade Layers) -- Implement progressive template features (PWA support) -- Deploy advanced animation and interaction system -- Enhance template performance monitoring and optimization - -#### 2031 (v08.00.00) - Next-Generation Template -- Advanced layout systems with subgrid support -- Comprehensive template component library -- Enhanced visual customization tools -- Template ecosystem with child themes and derivatives - ---- - -## External Resources - -### Official Links -- **Full Roadmap**: [https://mokoconsulting.tech/support/joomla-cms/mokocassiopeia-roadmap](https://mokoconsulting.tech/support/joomla-cms/mokocassiopeia-roadmap) -- **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia) -- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) -- **Changelog**: [CHANGELOG.md](../CHANGELOG.md) - -### Community -- **Email Support**: hello@mokoconsulting.tech -- **Contributing**: [CONTRIBUTING.md](../CONTRIBUTING.md) -- **Code of Conduct**: [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) - -### Documentation -- **Quick Start**: [docs/QUICK_START.md](./QUICK_START.md) -- **Workflow Guide**: [docs/WORKFLOW_GUIDE.md](./WORKFLOW_GUIDE.md) -- **Joomla Development**: [docs/JOOMLA_DEVELOPMENT.md](./JOOMLA_DEVELOPMENT.md) -- **Main README**: [README.md](../README.md) - ---- - -## Contributing to the Roadmap - -Have ideas for future features? We welcome community input! - -**How to Suggest Features**: -1. Check the [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) for existing requests -2. Open a new issue with the `enhancement` label -3. Provide clear use cases and benefits -4. Engage in community discussion - -**Feature Evaluation Criteria**: -- Alignment with core principles -- User demand and use cases -- Technical feasibility -- Maintenance burden -- Performance impact -- Security implications - ---- - -## Metadata - -* Document: docs/ROADMAP.md -* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia](https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia) -* Path: /docs/ROADMAP.md -* Owner: Moko Consulting -* Version: 03.06.03 -* Status: Active -* Effective Date: 2026-01-30 -* Classification: Public Open Source Documentation - -## Revision History - -| Date | Change Summary | Author | -| ---------- | ----------------------------------------------------- | --------------- | -| 2026-01-27 | Initial version-specific roadmap generated from codebase scan. | GitHub Copilot | -| 2026-01-27 | Added 5-year future roadmap with annual major version releases (v04-v08). | GitHub Copilot | -| 2026-01-27 | Refocused roadmap to concentrate on template-oriented features only. | GitHub Copilot | +1. Download MokoOnyx from [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v01) +2. Install via **System → Install → Extensions** +3. Set MokoOnyx as default in **System → Site Templates** +4. Visit any frontend page — settings are imported automatically +5. Uninstall MokoCassiopeia from **Extensions → Manage** diff --git a/src/templateDetails.xml b/src/templateDetails.xml index e1ae6c1..88d0621 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -45,7 +45,7 @@ Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting -

⚠️ MokoCassiopeia has been renamed to MokoOnyx

This template is no longer maintained. Please install MokoOnyx manually to continue receiving updates. MokoOnyx has the same features and will automatically import your MokoCassiopeia settings on first install.

Steps to migrate:

  1. Download MokoOnyx: mokoonyx-01.00.00.zip
  2. Install via System → Install → Extensions → Upload Package File
  3. MokoOnyx will automatically copy your settings and set itself as the default template
  4. Uninstall MokoCassiopeia from Extensions → Manage

Download MokoOnyx

License Joomla PHP

MokoCassiopeia is succeeded by MokoOnyx — same features, new name. All future development continues under the MokoOnyx project.

]]>
+

⚠️ MokoCassiopeia has been renamed to MokoOnyx

This template is no longer maintained. Please install MokoOnyx manually to continue receiving updates. MokoOnyx has the same features and will automatically import your MokoCassiopeia settings on first install.

Steps to migrate:

  1. Download MokoOnyx from Gitea Releases (v01)
  2. Install via System → Install → Extensions → Upload Package File
  3. Set MokoOnyx as default in System → Site Templates
  4. Visit any frontend page — your settings are imported automatically
  5. Uninstall MokoCassiopeia from Extensions → Manage

Download MokoOnyx

License Joomla PHP

MokoCassiopeia is succeeded by MokoOnyx — same features, new name. All future development continues under the MokoOnyx project.

]]>
1 component.php From 0998143630e4bf05af3c857e7885f926b8ab01c4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 14:34:21 -0500 Subject: [PATCH 41/41] docs: mark MokoCassiopeia as retired, link to MokoOnyx Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9f1710..9d70810 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ BRIEF: Documentation for MokoCassiopeia template --> -# MokoCassiopeia → MokoOnyx +# MokoCassiopeia (Retired) -> **This template is being renamed to MokoOnyx.** Version 03.10.14 is the bridge release that automatically migrates your settings. After updating, MokoOnyx will be your active template and MokoCassiopeia can be safely uninstalled. +> **This template has been retired and replaced by [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx).** MokoCassiopeia is no longer maintained. To migrate, install MokoOnyx from [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v01), set it as your default template, and visit any page — your settings will be imported automatically. Then uninstall MokoCassiopeia. -**A Modern, Lightweight Joomla Template Based on Cassiopeia** +**Retired — See [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx)** [![Version](https://img.shields.io/badge/version-03.09.07-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)