diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index b1bcc1ee..5b16dc77 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Package - MokoSuite MokoConsulting White-label identity, security hardening, and tenant restriction layer for Suite-managed Joomla environments - 02.34.50 + 02.34.55 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 92d94ac3..b4f6301d 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 02.34.50 +# VERSION: 02.34.55 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml new file mode 100644 index 00000000..f54b1840 --- /dev/null +++ b/.mokogitea/workflows/rc-revert.yml @@ -0,0 +1,66 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoPlatform.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/rc-revert.yml +# VERSION: 09.23.00 +# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge + +name: "RC Revert" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + revert: + name: Rename rc/ back to dev/ + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == false && + startsWith(github.event.pull_request.head.ref, 'rc/') + + steps: + - name: Rename branch + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + SUFFIX="${BRANCH#rc/}" + DEV_BRANCH="dev/${SUFFIX}" + API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Create dev/ branch from rc/ branch + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ + "${API}" 2>/dev/null || true) + + if [ "$STATUS" = "201" ]; then + echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})" + exit 1 + fi + + # Delete rc/ branch + ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${TOKEN}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" + fi + + echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY + echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c396ba..9155f2f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ INGROUP: MokoSuite.Documentation REPO: https://github.com/mokoconsulting-tech/mokosuite PATH: ./CHANGELOG.md - VERSION: 02.34.50 + VERSION: 02.34.55 BRIEF: Version history using `Keep a Changelog` --> @@ -23,6 +23,9 @@ ## [Unreleased] ### Added +- plg_system_mokosuite_dbip — IP geolocation plugin using DB-IP MMDB databases (CDN auto-download, local file mode, bundled MaxMind reader) +- Admin sidebar menu restructure — each Moko component gets its own collapsible section, com_mokosuitehq pinned first +- rc-revert workflow for release candidate rollbacks - RSA-signed heartbeat authentication — private key in monitor plugin manifest, public key on MokoSuiteHQ - Monitor plugin base_url set via manifest (hidden from admin UI), propagated via update server - Send Heartbeat button on health token field for manual heartbeat testing diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ecad5cb3..fa824126 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Documentation REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: ./CODE_OF_CONDUCT.md BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default --> diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2696ef49..c866f439 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.MokoSuiteBrand INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/MokoSuiteBrand - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoSuiteBrand --> diff --git a/LICENSE.md b/LICENSE.md index 23cc6208..3f83f007 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -15,7 +15,7 @@ INGROUP: MokoSuite.Documentation REPO: https://github.com/mokoconsulting-tech/mokosuite PATH: ./LICENSE.md - VERSION: 02.34.50 + VERSION: 02.34.55 BRIEF: Project license (GPL-3.0-or-later) --> GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index 1a9620cd..4c463de0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /README.md BRIEF: MokoSuite platform plugin for Joomla --> diff --git a/SECURITY.md b/SECURITY.md index bce7fda6..5c0a29d3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME] INGROUP: [PROJECT_NAME].Documentation REPO: [REPOSITORY_URL] PATH: /SECURITY.md -VERSION: 02.34.50 +VERSION: 02.34.55 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/docs/guides/build-guide.md b/docs/guides/build-guide.md index 96b47b06..5bf2dad0 100644 --- a/docs/guides/build-guide.md +++ b/docs/guides/build-guide.md @@ -11,13 +11,13 @@ INGROUP: MokoSuite.Build REPO: https://github.com/mokoconsulting-tech/mokosuite FILE: build-guide.md - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/ BRIEF: Build and packaging guide for the MokoSuite system plugin NOTE: Defines environment setup, repository layout, packaging rules, and release preparation --> -# MokoSuite Build Guide (VERSION: 02.34.50) +# MokoSuite Build Guide (VERSION: 02.34.55) ## 1. Purpose diff --git a/docs/guides/configuration-guide.md b/docs/guides/configuration-guide.md index 43c04cb9..60bc803f 100644 --- a/docs/guides/configuration-guide.md +++ b/docs/guides/configuration-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/configuration-guide.md BRIEF: Configuration guide for the MokoSuite system plugin NOTE: Defines plugin parameters, expected behaviors, and recommended defaults --> -# MokoSuite Configuration Guide (VERSION: 02.34.50) +# MokoSuite Configuration Guide (VERSION: 02.34.55) ## 1. Objective diff --git a/docs/guides/installation-guide.md b/docs/guides/installation-guide.md index 866f006f..bc2c38f6 100644 --- a/docs/guides/installation-guide.md +++ b/docs/guides/installation-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/installation-guide.md BRIEF: Installation guide for the MokoSuite system plugin NOTE: First document in the guide set --> -# MokoSuite Installation Guide (VERSION: 02.34.50) +# MokoSuite Installation Guide (VERSION: 02.34.55) ## Introduction diff --git a/docs/guides/operations-guide.md b/docs/guides/operations-guide.md index 6bcdcc18..6fe432be 100644 --- a/docs/guides/operations-guide.md +++ b/docs/guides/operations-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/operations-guide.md BRIEF: Operational guide for administering and managing the MokoSuite system plugin NOTE: Defines lifecycle, responsibilities, and operational behaviors --> -# MokoSuite Operations Guide (VERSION: 02.34.50) +# MokoSuite Operations Guide (VERSION: 02.34.55) ## Introduction diff --git a/docs/guides/rollback-and-recovery-guide.md b/docs/guides/rollback-and-recovery-guide.md index 6ec87d21..b38b8cae 100644 --- a/docs/guides/rollback-and-recovery-guide.md +++ b/docs/guides/rollback-and-recovery-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/rollback-and-recovery-guide.md BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents NOTE: Completes the core guide set for Suite plugin governance --> -# MokoSuite Rollback and Recovery Guide (VERSION: 02.34.50) +# MokoSuite Rollback and Recovery Guide (VERSION: 02.34.55) ## Introduction diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md index f257623d..07cf2afc 100644 --- a/docs/guides/testing-guide.md +++ b/docs/guides/testing-guide.md @@ -7,13 +7,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/testing-guide.md BRIEF: Testing guide for MokoSuite v02.01.08 NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration --> -# MokoSuite Testing Guide (VERSION: 02.34.50) +# MokoSuite Testing Guide (VERSION: 02.34.55) ## 1. Prerequisites diff --git a/docs/guides/troubleshooting-guide.md b/docs/guides/troubleshooting-guide.md index 29297cf6..aa93d45b 100644 --- a/docs/guides/troubleshooting-guide.md +++ b/docs/guides/troubleshooting-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/troubleshooting-guide.md BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuite plugin NOTE: Designed for administrators and Suite operations teams --> -# MokoSuite Troubleshooting Guide (VERSION: 02.34.50) +# MokoSuite Troubleshooting Guide (VERSION: 02.34.55) ## Introduction diff --git a/docs/guides/upgrade-and-versioning-guide.md b/docs/guides/upgrade-and-versioning-guide.md index b6975d4e..4b21b6a5 100644 --- a/docs/guides/upgrade-and-versioning-guide.md +++ b/docs/guides/upgrade-and-versioning-guide.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Guides REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/guides/upgrade-and-versioning-guide.md BRIEF: Guide for updating, versioning, and maintaining the MokoSuite plugin NOTE: Defines release flow, version rules, and upgrade validation --> -# MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.50) +# MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.55) ## Introduction diff --git a/docs/index.md b/docs/index.md index 45f8e2ee..6fb8d48f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,13 +10,13 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoSuite.Documentation REPO: https://github.com/mokoconsulting-tech/mokosuite - VERSION: 02.34.50 + VERSION: 02.34.55 PATH: /docs/index.md BRIEF: Master index of all documentation for the MokoSuite plugin NOTE: Automatically maintained index for all guide canvases --> -# MokoSuite Documentation Index (VERSION: 02.34.50) +# MokoSuite Documentation Index (VERSION: 02.34.55) ## Introduction diff --git a/docs/plugin-basic.md b/docs/plugin-basic.md index 0f559a1a..4518d197 100644 --- a/docs/plugin-basic.md +++ b/docs/plugin-basic.md @@ -11,12 +11,12 @@ INGROUP: MokoSuite REPO: https://github.com/mokoconsulting-tech/mokosuite PATH: /docs/plugin-basic.md - VERSION: 02.34.50 + VERSION: 02.34.55 BRIEF: Baseline documentation for the MokoSuite system plugin NOTE: Foundational reference for internal and external stakeholders --> -# MokoSuite Plugin Overview (VERSION: 02.34.50) +# MokoSuite Plugin Overview (VERSION: 02.34.55) ## Introduction diff --git a/docs/update-server.md b/docs/update-server.md index 4fd5d6b5..0c271bb9 100644 --- a/docs/update-server.md +++ b/docs/update-server.md @@ -10,7 +10,7 @@ DEFGROUP: MokoSuite.Documentation INGROUP: MokoStandards.Templates REPO: https://github.com/mokoconsulting-tech/MokoSuite PATH: /docs/update-server.md -VERSION: 02.34.50 +VERSION: 02.34.55 BRIEF: How this extension's Joomla update server file (update.xml) is managed --> diff --git a/source/packages/com_mokosuite/admin/src/Service/NotificationService.php b/source/packages/com_mokosuite/admin/src/Service/NotificationService.php index f0b94f77..27bf3c5b 100644 --- a/source/packages/com_mokosuite/admin/src/Service/NotificationService.php +++ b/source/packages/com_mokosuite/admin/src/Service/NotificationService.php @@ -70,6 +70,9 @@ class NotificationService Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } + + // Push notification via ntfy + self::pushNtfy($event, $ticket, $subject); } catch (\Throwable $e) { @@ -332,6 +335,159 @@ class NotificationService } } + // ================================================================== + // Ntfy Push Notifications (#205) + // ================================================================== + + /** + * Send a push notification via ntfy for ticket events. + */ + private static function pushNtfy(string $event, object $ticket, string $title): void + { + $config = self::getNotificationConfig(); + $ntfyEnabled = $config['ntfy_enabled'] ?? '0'; + + if (!$ntfyEnabled) + { + return; + } + + $ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/'); + $ntfyTopic = $config['ntfy_topic'] ?? 'mokosuite-tickets'; + $ntfyToken = $config['ntfy_token'] ?? ''; + + $tagMap = [ + 'ticket_created' => 'ticket,new', + 'ticket_replied' => 'speech_balloon', + 'status_changed' => 'arrows_counterclockwise', + 'ticket_assigned' => 'bust_in_silhouette', + ]; + + $priorityMap = [ + 'ticket_created' => '4', + 'ticket_replied' => '3', + 'status_changed' => '3', + 'ticket_assigned' => '3', + ]; + + $siteUrl = rtrim(Uri::root(), '/'); + $ticketUrl = $siteUrl . '/administrator/index.php?option=com_mokosuite&view=ticket&id=' . ($ticket->id ?? 0); + + $message = self::buildNtfyMessage($event, $ticket); + + $headers = [ + 'Title: ' . $title, + 'Priority: ' . ($priorityMap[$event] ?? '3'), + 'Tags: ' . ($tagMap[$event] ?? 'ticket'), + 'Click: ' . $ticketUrl, + ]; + + if ($ntfyToken !== '') + { + $headers[] = 'Authorization: Bearer ' . $ntfyToken; + } + + $url = $ntfyServer . '/' . $ntfyTopic; + + try + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_exec($ch); + + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode < 200 || $httpCode >= 300) + { + Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuite'); + } + } + catch (\Throwable $e) + { + Log::add('Ntfy push error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); + } + } + + /** + * Build a short ntfy message body for ticket events. + */ + private static function buildNtfyMessage(string $event, object $ticket): string + { + $subject = $ticket->subject ?? 'Ticket #' . ($ticket->id ?? '?'); + + switch ($event) + { + case 'ticket_created': + $priority = ucfirst($ticket->priority ?? 'normal'); + return "New ticket: {$subject}\nPriority: {$priority}"; + + case 'ticket_replied': + return "Reply on: {$subject}"; + + case 'status_changed': + $status = ucwords(str_replace('_', ' ', $ticket->status ?? '')); + return "Status → {$status}: {$subject}"; + + case 'ticket_assigned': + return "Assigned to you: {$subject}"; + + default: + return $subject; + } + } + + /** + * Send a push notification via ntfy for security events. + */ + public static function pushNtfySecurity(string $event, string $title, string $body): void + { + $config = self::getNotificationConfig(); + $ntfyEnabled = $config['ntfy_enabled'] ?? '0'; + + if (!$ntfyEnabled) + { + return; + } + + $ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/'); + $ntfyTopic = $config['ntfy_security_topic'] ?? $config['ntfy_topic'] ?? 'mokosuite-security'; + $ntfyToken = $config['ntfy_token'] ?? ''; + + $headers = [ + 'Title: [Security] ' . $title, + 'Priority: 5', + 'Tags: warning,shield', + ]; + + if ($ntfyToken !== '') + { + $headers[] = 'Authorization: Bearer ' . $ntfyToken; + } + + $url = $ntfyServer . '/' . $ntfyTopic; + + try + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_exec($ch); + curl_close($ch); + } + catch (\Throwable $e) + { + Log::add('Ntfy security push error: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); + } + } + // ================================================================== // Security Event Notifications (#131) // ================================================================== @@ -407,6 +563,9 @@ class NotificationService Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokosuite'); } } + + // Also push via ntfy + self::pushNtfySecurity($event, $subject, $body); } catch (\Throwable $e) { diff --git a/source/packages/com_mokosuite/mokosuite.xml b/source/packages/com_mokosuite/mokosuite.xml index 645ca47d..5559dfc7 100644 --- a/source/packages/com_mokosuite/mokosuite.xml +++ b/source/packages/com_mokosuite/mokosuite.xml @@ -20,7 +20,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.34.50-dev + 02.34.55-dev MokoSuite admin dashboard and REST API. Provides a control panel for managing MokoSuite feature plugins, site health monitoring, and remote management endpoints. Moko\Component\MokoSuite diff --git a/source/packages/mod_mokosuite_cache/mod_mokosuite_cache.xml b/source/packages/mod_mokosuite_cache/mod_mokosuite_cache.xml index 7a89d358..6460d428 100644 --- a/source/packages/mod_mokosuite_cache/mod_mokosuite_cache.xml +++ b/source/packages/mod_mokosuite_cache/mod_mokosuite_cache.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.34.50-dev + 02.34.55-dev MOD_MOKOSUITE_CACHE_DESC Moko\Module\MokoSuiteCache diff --git a/source/packages/mod_mokosuite_categories/mod_mokosuite_categories.xml b/source/packages/mod_mokosuite_categories/mod_mokosuite_categories.xml index 6b9cda2b..4b8b7c72 100644 --- a/source/packages/mod_mokosuite_categories/mod_mokosuite_categories.xml +++ b/source/packages/mod_mokosuite_categories/mod_mokosuite_categories.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.34.50-dev + 02.34.55-dev MOD_MOKOSUITE_CATEGORIES_DESC Moko\Module\MokoSuiteCategories diff --git a/source/packages/mod_mokosuite_cpanel/mod_mokosuite_cpanel.xml b/source/packages/mod_mokosuite_cpanel/mod_mokosuite_cpanel.xml index e4c07e69..51814dec 100644 --- a/source/packages/mod_mokosuite_cpanel/mod_mokosuite_cpanel.xml +++ b/source/packages/mod_mokosuite_cpanel/mod_mokosuite_cpanel.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.34.50-dev + 02.34.55-dev MOD_MOKOSUITE_CPANEL_DESC Moko\Module\MokoSuiteCpanel diff --git a/source/packages/mod_mokosuite_menu/mod_mokosuite_menu.xml b/source/packages/mod_mokosuite_menu/mod_mokosuite_menu.xml index 3cb6de06..6b8498bd 100644 --- a/source/packages/mod_mokosuite_menu/mod_mokosuite_menu.xml +++ b/source/packages/mod_mokosuite_menu/mod_mokosuite_menu.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.34.50-dev + 02.34.55-dev MokoSuite admin sidebar menu — renders a dedicated MokoSuite section in the admin menu before Joomla's default menu. Moko\Module\MokoSuiteMenu diff --git a/source/packages/mod_mokosuite_menu/tmpl/default.php b/source/packages/mod_mokosuite_menu/tmpl/default.php index 63d658f9..86447f21 100644 --- a/source/packages/mod_mokosuite_menu/tmpl/default.php +++ b/source/packages/mod_mokosuite_menu/tmpl/default.php @@ -2,9 +2,9 @@ /** * MokoSuite Admin Sidebar Menu * - * Renders MokoSuite static views first, then auto-discovers installed - * Moko components from #__menu and renders their submenu items as - * nested MetisMenu collapsible sections. + * Each installed Moko component gets its own top-level collapsible section. + * com_mokosuitehq is always pinned first. com_mokosuite uses static views + * as children. All other components auto-discover their submenu items. */ defined('_JEXEC') or die; @@ -17,8 +17,8 @@ $app = Factory::getApplication(); $currentOption = $app->getInput()->get('option', ''); $currentView = $app->getInput()->get('view', ''); -// ── Static MokoSuite views ──────────────────────────────────────────── -$mokosuiteItems = [ +// ── Static views for com_mokosuite ────────────────────────────────── +$mokosuiteStaticViews = [ ['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuite'], ['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokosuite&view=tickets'], ['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuite&view=extensions'], @@ -30,27 +30,25 @@ $mokosuiteItems = [ ['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuite'], ]; -// ── Auto-discover Moko component menus from #__menu ────────────────── +// ── Auto-discover all Moko components from #__menu ────────────────── $mokoComponents = []; try { $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class); - // Find all Moko component menu items (exclude com_mokosuite — handled above) $db->setQuery( "SELECT m.id, m.title, m.link, m.level, m.parent_id, m.img, e.element" . " FROM " . $db->quoteName('#__menu') . " m" . " LEFT JOIN " . $db->quoteName('#__extensions') . " e ON m.component_id = e.extension_id" . " WHERE m.client_id = 1 AND m.level >= 1 AND m.published = 1" . " AND e.element LIKE 'com_moko%'" - . " AND e.element != 'com_mokosuite'" . " AND e.enabled = 1" . " ORDER BY e.element, m.level, m.lft" ); $menuItems = $db->loadObjectList() ?: []; - // Load sys.ini language files for discovered components + // Load language files for discovered components $lang = Factory::getLanguage(); $loadedLangs = []; foreach ($menuItems as $m) @@ -92,100 +90,112 @@ catch (\Throwable $e) // Silent — menu works without auto-discovered components } -// ── Determine active state ─────────────────────────────────────────── -$mokosuiteActive = ($currentOption === 'com_mokosuite'); -$anyMokoActive = $mokosuiteActive; - -foreach ($mokoComponents as $comp) +// Override com_mokosuite children with static views +if (isset($mokoComponents['com_mokosuite'])) { - $parsed = []; - parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $parsed); - if (($parsed['option'] ?? '') === $currentOption) + $mokoComponents['com_mokosuite']['children'] = $mokosuiteStaticViews; + $mokoComponents['com_mokosuite']['icon'] = 'icon-shield-alt'; +} +else +{ + // com_mokosuite not in admin menu — add it manually + $mokoComponents['com_mokosuite'] = [ + 'id' => 0, + 'title' => 'MokoSuite', + 'link' => 'index.php?option=com_mokosuite', + 'icon' => 'icon-shield-alt', + 'element' => 'com_mokosuite', + 'children' => $mokosuiteStaticViews, + ]; +} + +// ── Sort: com_mokosuitehq first, then alphabetical by title ───────── +$hq = null; +$rest = []; + +foreach ($mokoComponents as $key => $comp) +{ + if ($key === 'com_mokosuitehq') { - $anyMokoActive = true; + $hq = $comp; + } + else + { + $rest[$key] = $comp; } } -$topClass = 'item parent item-level-1' . ($anyMokoActive ? ' mm-active' : ''); -$topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' : ''); +usort($rest, fn($a, $b) => strcasecmp($a['title'], $b['title'])); + +$sorted = []; +if ($hq !== null) +{ + $sorted[] = $hq; +} +foreach ($rest as $comp) +{ + $sorted[] = $comp; +} ?>