52 Commits

Author SHA1 Message Date
gitea-actions[bot] d898de5bdc chore(version): pre-release bump to 02.51.03-dev [skip ci] 2026-06-28 18:57:24 +00:00
gitea-actions[bot] dfb0e22912 chore(version): pre-release bump to 02.51.02-dev [skip ci] 2026-06-28 18:56:35 +00:00
gitea-actions[bot] 199d3da05e chore(version): auto-bump patch 02.48.53-dev [skip ci] 2026-06-28 18:56:25 +00:00
jmiller 9417bb60cb feat: add Conditions, Snippets, Replacements, Templates, Modules views
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Five new admin views with models, templates, and list UI:
- Conditions: condition sets with group/rule counts and inline publish
- Snippets: reusable text blocks with {snippet alias} syntax
- Replacements: search/replace rules with regex and area badges
- Templates: content templates with category and description
- Modules: advanced module manager with position and client badges
Also adds togglePublished endpoint to DisplayController.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 13:55:58 -05:00
gitea-actions[bot] 33998a1685 chore(version): pre-release bump to 02.48.52-dev [skip ci] 2026-06-28 18:17:00 +00:00
gitea-actions[bot] c2d1a8a0e8 chore(version): auto-bump patch 02.48.51-dev [skip ci] 2026-06-28 18:16:48 +00:00
jmiller 68dd129c0f fix: XSS escaping in menu, SPDX header, orphaned docblock, getDbo()
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 14s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 35s
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- htmlspecialchars() on all icon/title output in menu module
- SPDX license header on cache Dispatcher
- Moved orphaned requestNew() docblock to correct location
- Replaced deprecated Factory::getDbo() with DI container pattern

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 13:16:30 -05:00
gitea-actions[bot] 5584e09ecd chore(version): pre-release bump to 02.48.50-dev [skip ci] 2026-06-28 13:02:37 +00:00
gitea-actions[bot] 931d93e921 chore(version): pre-release bump to 02.48.49-dev [skip ci] 2026-06-28 00:34:04 +00:00
gitea-actions[bot] dbcc02e1a4 chore(version): pre-release bump to 02.48.48-dev [skip ci] 2026-06-28 00:33:38 +00:00
gitea-actions[bot] efeb996703 chore(version): auto-bump patch 02.48.47-dev [skip ci] 2026-06-28 00:33:25 +00:00
jmiller 1002c55147 fix: unique icons for every MokoSuite component in admin menu
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Community=comments, HRM=id-badge, OpenGraph=globe, MRP=cogs.
No two components share the same icon now.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 19:32:47 -05:00
gitea-actions[bot] d7f2baeb3e chore(version): pre-release bump to 02.48.46-dev [skip ci] 2026-06-28 00:13:44 +00:00
gitea-actions[bot] 4d4a75cc52 chore(version): pre-release bump to 02.48.45-dev [skip ci] 2026-06-28 00:13:27 +00:00
gitea-actions[bot] ce26dab8fd chore(version): auto-bump patch 02.48.44-dev [skip ci] 2026-06-28 00:13:14 +00:00
jmiller d65d8faf65 feat: single MokoSuite menu item with collapsible ecosystem children
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Consolidates all Moko components under one top-level "MokoSuite"
sidebar entry. Each component with subviews is a nested collapsible.
Also: Help link keeps target=_blank but hides external-link icon.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 19:12:59 -05:00
gitea-actions[bot] 6332405853 chore(version): pre-release bump to 02.48.43-dev [skip ci] 2026-06-27 20:46:26 +00:00
gitea-actions[bot] 47f15e4dbb chore(version): auto-bump patch 02.48.42-dev [skip ci] 2026-06-27 20:46:07 +00:00
jmiller a232f2d3b7 fix: use ASCII-safe description in package manifest
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Replace em dash with colon to prevent encoding corruption in builds.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 15:45:50 -05:00
gitea-actions[bot] e6fdda02da chore(version): pre-release bump to 02.48.41-dev [skip ci] 2026-06-27 20:45:12 +00:00
gitea-actions[bot] 987e4e4662 chore(version): pre-release bump to 02.48.40-dev [skip ci] 2026-06-27 20:44:43 +00:00
gitea-actions[bot] 173c20164a chore(version): auto-bump patch 02.48.39-dev [skip ci] 2026-06-27 20:44:27 +00:00
jmiller 2a2240b2be fix: remove target=_blank from Help menu redirect
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Atum shows an external-link icon for _blank links, disrupting the
sidebar flow. The help link now opens in the same window.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 15:44:07 -05:00
gitea-actions[bot] f7cd0851c8 chore(version): pre-release bump to 02.48.38-dev [skip ci] 2026-06-27 20:33:59 +00:00
gitea-actions[bot] ea84e53d48 chore(version): pre-release bump to 02.48.37-dev [skip ci] 2026-06-27 20:33:30 +00:00
gitea-actions[bot] 3196cae2e5 chore(version): auto-bump patch 02.48.36-dev [skip ci] 2026-06-27 20:33:12 +00:00
jmiller 009bc3a8be feat: add icon overrides for all Moko components in admin menu
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Parent icons from catalog.xml, child icons auto-guessed from view
name (dashboard, contacts, orders, etc.) with fallback to angle-right.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 15:32:35 -05:00
gitea-actions[bot] cae2831fb1 chore(version): pre-release bump to 02.48.35-dev [skip ci] 2026-06-27 20:03:37 +00:00
gitea-actions[bot] cab1cd7ed8 chore(version): pre-release bump to 02.48.34-dev [skip ci] 2026-06-27 20:03:19 +00:00
gitea-actions[bot] 1f73f70fc2 chore(version): auto-bump patch 02.48.33-dev [skip ci] 2026-06-27 20:03:04 +00:00
jmiller 25baf6afd6 feat: merge Cache and Temp buttons into single Clean dropdown
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Reduces header bar footprint — Site and PIN buttons stay visible,
Cache and Temp are under a Clean ▾ dropdown menu.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 15:02:46 -05:00
gitea-actions[bot] 9148548c62 chore(version): pre-release bump to 02.48.32-dev [skip ci] 2026-06-27 19:51:16 +00:00
gitea-actions[bot] 4731ef6100 chore(version): pre-release bump to 02.48.31-dev [skip ci] 2026-06-27 19:50:59 +00:00
gitea-actions[bot] 32fa117569 chore(version): auto-bump patch 02.48.30-dev [skip ci] 2026-06-27 19:50:49 +00:00
jmiller a8b9f7d165 feat: cpanel module slim bar with collapsible detail panel
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
Replaces full-width card with a compact bar showing site name,
version, status badges, PIN, and IP. Click chevron to expand
the detail panel with environment, stats, disk, and plugin info.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-27 14:50:32 -05:00
gitea-actions[bot] 0288af9421 chore(version): pre-release bump to 02.48.29-dev [skip ci] 2026-06-27 19:37:24 +00:00
gitea-actions[bot] 5a16c563f6 chore(version): pre-release bump to 02.48.28-dev [skip ci] 2026-06-27 02:20:21 +00:00
gitea-actions[bot] 92016a91e5 chore(version): auto-bump patch 02.48.27-dev [skip ci] 2026-06-27 02:20:11 +00:00
jmiller 94a0bbb160 fix: remove duplicate <install> block from component manifest
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
The second <install> block referenced admin/sql/install.mysql.sql
which resolved to a non-existent path during Joomla package updates,
causing "Install path does not exist" error.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-26 21:20:00 -05:00
gitea-actions[bot] 3673ca0525 chore(version): pre-release bump to 02.48.26-dev [skip ci] 2026-06-27 00:34:55 +00:00
gitea-actions[bot] 6b6c963bb7 chore(version): auto-bump patch 02.48.25-dev [skip ci] 2026-06-27 00:34:45 +00:00
jmiller 13ce8c6eeb fix: add missing RL destination tables to SQL updates, fix import detection
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Sites that upgraded never got mokosuiteclient_conditions, _snippets,
_replacements, _content_templates tables — only fresh installs did.
Import banner now requires both source AND destination tables.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-26 19:34:33 -05:00
gitea-actions[bot] bf9c94bbf8 chore(version): pre-release bump to 02.48.24-dev [skip ci] 2026-06-25 19:45:58 +00:00
gitea-actions[bot] 19d4b63ca6 chore(version): pre-release bump to 02.48.23-dev [skip ci] 2026-06-25 17:47:17 +00:00
gitea-actions[bot] 95e0ff04a4 chore(version): pre-release bump to 02.48.22-dev [skip ci] 2026-06-25 17:47:03 +00:00
gitea-actions[bot] 57748a2a23 chore(version): auto-bump patch 02.48.21-dev [skip ci] 2026-06-25 17:46:55 +00:00
jmiller 3f4f2f2d43 feat: auto-enrich IPs with country flags across all admin pages
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
DB-IP plugin onAfterRender scans admin HTML for IP addresses in
<code> tags, looks up geolocation, prepends country flag emoji and
adds city/region/country hover tooltip. Caches lookups per request,
skips private/loopback IPs and already-enriched elements.
2026-06-25 12:46:42 -05:00
gitea-actions[bot] 7ac776ff28 chore(version): pre-release bump to 02.48.20-dev [skip ci] 2026-06-25 17:43:34 +00:00
gitea-actions[bot] 89b71a5a65 chore(version): pre-release bump to 02.48.19-dev [skip ci] 2026-06-25 17:39:16 +00:00
gitea-actions[bot] 0b3eb7c7aa chore(version): pre-release bump to 02.48.18-dev [skip ci] 2026-06-25 17:38:56 +00:00
gitea-actions[bot] 4b809778f4 chore(version): auto-bump patch 02.48.17-dev [skip ci] 2026-06-25 17:38:43 +00:00
jmiller 7ab1fe4cdd fix: PIN copy revert timeout changed to 5 seconds
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
2026-06-25 12:38:25 -05:00
67 changed files with 1991 additions and 208 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation # INGROUP: moko-platform.Automation
# VERSION: 02.48.16 # VERSION: 02.51.03
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+6 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoSuiteClient.Documentation INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./CHANGELOG.md PATH: ./CHANGELOG.md
VERSION: 02.48.16 VERSION: 02.51.03
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
@@ -64,6 +64,11 @@
- **Update server migration** — removed migrateUpdateServerUrls, cleanupStaleUpdateSites, fixUpdateRecords, enableUpdateServer calls - **Update server migration** — removed migrateUpdateServerUrls, cleanupStaleUpdateSites, fixUpdateRecords, enableUpdateServer calls
### Fixed ### Fixed
- **Regular Labs import** — destination tables missing from SQL update files; sites that upgraded never got the tables, causing "No data found" on import
- **Regular Labs import banner** — detection now requires both source AND destination tables before showing the import button
- **DB-IP auto-enrichment** — all IPs in `<code>` tags in admin backend now show country flag emoji and geo tooltip on hover
- **MokoSuiteBackup quick action** — dashboard now includes MokoSuiteBackup button when component is installed
- **PIN copy** — fixed duplicate click handlers (4 toast messages), "Copied!" not reverting, added "Click to copy" hover tooltip
- Health endpoint cron check SQL error — orphan `setQuery(getQuery(true), 0, 5)` produced bare `LIMIT 5`, returning 503 for all health polls - Health endpoint cron check SQL error — orphan `setQuery(getQuery(true), 0, 5)` produced bare `LIMIT 5`, returning 503 for all health polls
- License plugin missing `src/` and `language/` directories causing install failure - License plugin missing `src/` and `language/` directories causing install failure
- PIN generation inconsistency — controller used `floor(now/TTL)` while display used `floor(requestedAt/TTL)` - PIN generation inconsistency — controller used `floor(now/TTL)` while display used `floor(requestedAt/TTL)`
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: ./CODE_OF_CONDUCT.md PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
--> -->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand
INGROUP: MokoStandards.Governance INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /GOVERNANCE.md PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand
--> -->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoSuiteClient.Documentation INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./LICENSE.md PATH: ./LICENSE.md
VERSION: 02.48.16 VERSION: 02.51.03
BRIEF: Project license (GPL-3.0-or-later) BRIEF: Project license (GPL-3.0-or-later)
--> -->
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient INGROUP: MokoSuiteClient
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /README.md PATH: /README.md
BRIEF: MokoSuiteClient platform plugin for Joomla BRIEF: MokoSuiteClient platform plugin for Joomla
--> -->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL] REPO: [REPOSITORY_URL]
PATH: /SECURITY.md PATH: /SECURITY.md
VERSION: 02.48.16 VERSION: 02.51.03
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoSuiteClient.Build INGROUP: MokoSuiteClient.Build
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
FILE: build-guide.md FILE: build-guide.md
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/ PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoSuiteClient system plugin BRIEF: Build and packaging guide for the MokoSuiteClient system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
--> -->
# MokoSuiteClient Build Guide (VERSION: 02.48.16) # MokoSuiteClient Build Guide (VERSION: 02.51.03)
## 1. Purpose ## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/configuration-guide.md PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoSuiteClient system plugin BRIEF: Configuration guide for the MokoSuiteClient system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
--> -->
# MokoSuiteClient Configuration Guide (VERSION: 02.48.16) # MokoSuiteClient Configuration Guide (VERSION: 02.51.03)
## 1. Objective ## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/installation-guide.md PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoSuiteClient system plugin BRIEF: Installation guide for the MokoSuiteClient system plugin
NOTE: First document in the guide set NOTE: First document in the guide set
--> -->
# MokoSuiteClient Installation Guide (VERSION: 02.48.16) # MokoSuiteClient Installation Guide (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/operations-guide.md PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors NOTE: Defines lifecycle, responsibilities, and operational behaviors
--> -->
# MokoSuiteClient Operations Guide (VERSION: 02.48.16) # MokoSuiteClient Operations Guide (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/rollback-and-recovery-guide.md PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for Suite plugin governance NOTE: Completes the core guide set for Suite plugin governance
--> -->
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.48.16) # MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/testing-guide.md PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoSuiteClient v02.01.08 BRIEF: Testing guide for MokoSuiteClient v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
--> -->
# MokoSuiteClient Testing Guide (VERSION: 02.48.16) # MokoSuiteClient Testing Guide (VERSION: 02.51.03)
## 1. Prerequisites ## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/troubleshooting-guide.md PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin
NOTE: Designed for administrators and Suite operations teams NOTE: Designed for administrators and Suite operations teams
--> -->
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.48.16) # MokoSuiteClient Troubleshooting Guide (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/guides/upgrade-and-versioning-guide.md PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin
NOTE: Defines release flow, version rules, and upgrade validation NOTE: Defines release flow, version rules, and upgrade validation
--> -->
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.48.16) # MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.16 VERSION: 02.51.03
PATH: /docs/index.md PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoSuiteClient plugin BRIEF: Master index of all documentation for the MokoSuiteClient plugin
NOTE: Automatically maintained index for all guide canvases NOTE: Automatically maintained index for all guide canvases
--> -->
# MokoSuiteClient Documentation Index (VERSION: 02.48.16) # MokoSuiteClient Documentation Index (VERSION: 02.51.03)
## Introduction ## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoSuiteClient INGROUP: MokoSuiteClient
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: /docs/plugin-basic.md PATH: /docs/plugin-basic.md
VERSION: 02.48.16 VERSION: 02.51.03
BRIEF: Baseline documentation for the MokoSuiteClient system plugin BRIEF: Baseline documentation for the MokoSuiteClient system plugin
NOTE: Foundational reference for internal and external stakeholders NOTE: Foundational reference for internal and external stakeholders
--> -->
# MokoSuiteClient Plugin Overview (VERSION: 02.48.16) # MokoSuiteClient Plugin Overview (VERSION: 02.51.03)
## Introduction ## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoSuiteClient.Documentation
INGROUP: MokoStandards.Templates INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient
PATH: /docs/update-server.md PATH: /docs/update-server.md
VERSION: 02.48.16 VERSION: 02.51.03
BRIEF: How this extension's Joomla update server file (update.xml) is managed BRIEF: How this extension's Joomla update server file (update.xml) is managed
--> -->
@@ -0,0 +1,108 @@
-- Regular Labs replacement tables (conditions, snippets, replacements, content templates)
-- These were in install.mysql.sql but missing from updates, so existing installs never got them.
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`hash` VARCHAR(32) NOT NULL DEFAULT '',
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_groups` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`condition_id` INT UNSIGNED NOT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_condition` (`condition_id`),
KEY `idx_ordering` (`condition_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_rules` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`group_id` INT UNSIGNED NOT NULL,
`type` VARCHAR(50) NOT NULL DEFAULT '',
`exclude` TINYINT(1) NOT NULL DEFAULT 0,
`params` TEXT NOT NULL,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_group` (`group_id`),
KEY `idx_type` (`type`),
KEY `idx_ordering` (`group_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_map` (
`condition_id` INT UNSIGNED NOT NULL,
`extension` VARCHAR(50) NOT NULL DEFAULT '',
`item_id` INT UNSIGNED NOT NULL DEFAULT 0,
UNIQUE KEY `idx_unique` (`condition_id`, `item_id`, `extension`),
KEY `idx_ext_item` (`extension`, `item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_snippets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`content` MEDIUMTEXT NOT NULL,
`params` TEXT NOT NULL,
`published` TINYINT(1) NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_alias` (`alias`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_replacements` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL DEFAULT '',
`search` TEXT NOT NULL,
`replace_value` TEXT NOT NULL,
`area` VARCHAR(20) NOT NULL DEFAULT 'both',
`regex` TINYINT(1) NOT NULL DEFAULT 0,
`casesensitive` TINYINT(1) NOT NULL DEFAULT 0,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`published` TINYINT(1) NOT NULL DEFAULT 0,
`description` TEXT NOT NULL,
`enable_in_admin` TINYINT(1) NOT NULL DEFAULT 0,
`color` VARCHAR(8) DEFAULT NULL,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_content_templates` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`template_data` MEDIUMTEXT NOT NULL,
`joomla_category_id` INT NOT NULL DEFAULT 0,
`access` INT UNSIGNED NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`),
KEY `idx_category` (`joomla_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -36,6 +36,7 @@ class DisplayController extends BaseController
'templates' => 'mokosuiteclient.templates.manage', 'templates' => 'mokosuiteclient.templates.manage',
'replacements' => 'mokosuiteclient.replacements.manage', 'replacements' => 'mokosuiteclient.replacements.manage',
'conditions' => 'mokosuiteclient.conditions.manage', 'conditions' => 'mokosuiteclient.conditions.manage',
'modules' => 'core.admin',
]; ];
public function display($cachable = false, $urlparams = []) public function display($cachable = false, $urlparams = [])
@@ -800,6 +801,61 @@ class DisplayController extends BaseController
$this->jsonResponse($this->getModel('Import')->importAdminTools()); $this->jsonResponse($this->getModel('Import')->importAdminTools());
} }
// ==================================================================
// Toggle Published
// ==================================================================
public function togglePublished()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
$table = $app->getInput()->getString('table', '');
$id = $app->getInput()->getInt('id', 0);
$allowed = ['mokosuiteclient_conditions', 'mokosuiteclient_snippets',
'mokosuiteclient_replacements', 'mokosuiteclient_content_templates', 'modules'];
if (!in_array($table, $allowed, true) || $id <= 0)
{
$this->jsonResponse(['success' => false, 'message' => 'Invalid table or ID.']);
return;
}
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$dbTable = '#__' . $table;
$current = (int) $db->setQuery(
$db->getQuery(true)
->select($db->quoteName('published'))
->from($db->quoteName($dbTable))
->where($db->quoteName('id') . ' = ' . $id)
)->loadResult();
$newState = $current ? 0 : 1;
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName($dbTable))
->set($db->quoteName('published') . ' = ' . $newState)
->where($db->quoteName('id') . ' = ' . $id)
)->execute();
$this->jsonResponse(['success' => true, 'published' => $newState]);
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
}
// ================================================================== // ==================================================================
// Helpers // Helpers
// ================================================================== // ==================================================================
@@ -108,13 +108,6 @@ class SupportPinHelper
return 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4)); return 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
} }
/**
* Request a new PIN: stamps the current time into plugin params and returns the PIN.
*
* @param DatabaseInterface $db Database driver.
*
* @return array{success: bool, pin?: string, message: string}
*/
/** /**
* Render PIN badge HTML (active PIN with copy, or request button). * Render PIN badge HTML (active PIN with copy, or request button).
* *
@@ -198,7 +191,7 @@ class SupportPinHelper
navigator.clipboard.writeText(pin).then(function() { navigator.clipboard.writeText(pin).then(function() {
if (textEl) { if (textEl) {
textEl.textContent = 'Copied!'; textEl.textContent = 'Copied!';
setTimeout(function() { textEl.textContent = pin; }, 30000); setTimeout(function() { textEl.textContent = pin; }, 5000);
} }
}); });
} }
@@ -257,6 +250,13 @@ class SupportPinHelper
JS; JS;
} }
/**
* Request a new PIN: stamps the current time into plugin params and returns the PIN.
*
* @param DatabaseInterface $db Database driver.
*
* @return array{success: bool, pin?: string, message: string}
*/
public static function requestNew(DatabaseInterface $db): array public static function requestNew(DatabaseInterface $db): array
{ {
$state = self::getState($db); $state = self::getState($db);
@@ -0,0 +1,106 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ConditionsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('c.id'),
$db->quoteName('c.alias'),
$db->quoteName('c.name'),
$db->quoteName('c.description'),
$db->quoteName('c.category'),
$db->quoteName('c.color'),
$db->quoteName('c.match_all'),
$db->quoteName('c.published'),
])
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('c.name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
public function getGroupCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_groups'))
->where($db->quoteName('condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
public function getRuleCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_rules', 'r'))
->join('INNER', $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id'))
->where($db->quoteName('g.condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ModulesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('m.id'),
$db->quoteName('m.title'),
$db->quoteName('m.module'),
$db->quoteName('m.position'),
$db->quoteName('m.published'),
$db->quoteName('m.ordering'),
$db->quoteName('m.client_id'),
$db->quoteName('m.access'),
$db->quoteName('m.language'),
])
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$query->order($db->quoteName('m.client_id') . ' ASC, '
. $db->quoteName('m.position') . ' ASC, '
. $db->quoteName('m.ordering') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ReplacementsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class SnippetsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class TemplatesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,61 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Conditions;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ConditionsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
foreach ($this->items as $item)
{
$item->group_count = $model->getGroupCount((int) $item->id);
$item->rule_count = $model->getRuleCount((int) $item->id);
}
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Conditions', 'shuffle');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -44,15 +44,16 @@ class HtmlView extends BaseHtmlView
$this->supportPinAvailable = $pinState['available']; $this->supportPinAvailable = $pinState['available'];
$this->supportPin = $pinState['pin']; $this->supportPin = $pinState['pin'];
// Detect Regular Labs data for import // Detect Regular Labs data for import (source table must exist AND our destination table)
try { try {
$rlDb = \Joomla\CMS\Factory::getDbo(); $rlDb = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$rlTables = $rlDb->getTableList(); $rlTables = $rlDb->getTableList();
$rlPrefix = $rlDb->getPrefix(); $rlPrefix = $rlDb->getPrefix();
$this->regularLabsAvailable = in_array($rlPrefix . 'conditions', $rlTables) $this->regularLabsAvailable =
|| in_array($rlPrefix . 'snippets', $rlTables) (in_array($rlPrefix . 'conditions', $rlTables) && in_array($rlPrefix . 'mokosuiteclient_conditions', $rlTables))
|| in_array($rlPrefix . 'rereplacer', $rlTables) || (in_array($rlPrefix . 'snippets', $rlTables) && in_array($rlPrefix . 'mokosuiteclient_snippets', $rlTables))
|| in_array($rlPrefix . 'contenttemplater', $rlTables); || (in_array($rlPrefix . 'rereplacer', $rlTables) && in_array($rlPrefix . 'mokosuiteclient_replacements', $rlTables))
|| (in_array($rlPrefix . 'contenttemplater', $rlTables) && in_array($rlPrefix . 'mokosuiteclient_content_templates', $rlTables));
} catch (\Throwable $e) {} } catch (\Throwable $e) {}
$this->recentLogins = $model->getRecentLogins(10); $this->recentLogins = $model->getRecentLogins(10);
@@ -0,0 +1,56 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Modules;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ModulesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
'client_id' => $input->get('filter_client', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Module Manager', 'cube');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Replacements;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ReplacementsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Replacements', 'right-left');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Snippets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\SnippetsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Snippets', 'code');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Templates;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\TemplatesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Content Templates', 'file-lines');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-conditions">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="conditions">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-shuffle"></span> Conditions</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Match</th>
<th style="width:8%">Groups</th>
<th style="width:8%">Rules</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No conditions found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><span class="badge bg-<?php echo $item->match_all ? 'primary' : 'warning text-dark'; ?>"><?php echo $item->match_all ? 'ALL' : 'ANY'; ?></span></td>
<td><?php echo (int) $item->group_count; ?></td>
<td><?php echo (int) $item->rule_count; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_conditions" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
});
});
});
});
</script>
@@ -0,0 +1,149 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
$publishedLabels = [1 => 'Published', 0 => 'Unpublished', -2 => 'Trashed'];
$publishedColors = [1 => 'success', 0 => 'danger', -2 => 'dark'];
?>
<div id="mokosuiteclient-modules">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="modules">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search title, type, or position..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_client" class="form-select form-select-sm">
<option value="">All Clients</option>
<option value="0"<?php echo $filters['client_id'] === '0' ? ' selected' : ''; ?>>Site</option>
<option value="1"<?php echo $filters['client_id'] === '1' ? ' selected' : ''; ?>>Administrator</option>
</select>
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
<option value="-2"<?php echo $filters['published'] === '-2' ? ' selected' : ''; ?>>Trashed</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-cube"></span> Module Manager</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Title</th>
<th>Position</th>
<th>Type</th>
<th style="width:8%">Client</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="7" class="text-center text-muted py-4">No modules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<a href="<?php echo Route::_('index.php?option=com_modules&task=module.edit&id=' . (int) $item->id); ?>">
<?php echo htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</td>
<td><code><?php echo htmlspecialchars($item->position ?: '(none)', ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><small><?php echo htmlspecialchars($item->module, ENT_QUOTES, 'UTF-8'); ?></small></td>
<td><span class="badge bg-<?php echo $item->client_id ? 'dark' : 'primary'; ?>"><?php echo $item->client_id ? 'Admin' : 'Site'; ?></span></td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<?php
$pub = (int) $item->published;
$label = $publishedLabels[$pub] ?? 'Unknown';
$color = $publishedColors[$pub] ?? 'secondary';
?>
<a href="#" class="mokosuite-toggle-module badge bg-<?php echo $color; ?>"
data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $label; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')
. ($filters['client_id'] !== '' ? '&filter_client=' . $filters['client_id'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-module').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', 'modules');
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-module badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
});
});
});
});
</script>
@@ -0,0 +1,139 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-replacements">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="replacements">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or pattern..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-right-left"></span> Replacements</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Search</th>
<th>Replace</th>
<th style="width:7%">Area</th>
<th style="width:5%">Regex</th>
<th>Category</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No replacement rules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->search, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->replace_value, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><span class="badge bg-secondary"><?php echo htmlspecialchars($item->area, ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?php echo $item->regex ? '<span class="badge bg-warning text-dark">Yes</span>' : ''; ?></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_replacements" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
});
});
});
});
</script>
@@ -0,0 +1,138 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-snippets">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="snippets">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-code"></span> Snippets</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No snippets found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code>{snippet <?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?>}</code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_snippets" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
});
});
});
});
</script>
@@ -0,0 +1,138 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-templates">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="templates">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-file-alt"></span> Content Templates</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No content templates found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_content_templates" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
});
});
});
});
</script>
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description> <description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoSuiteClient</namespace> <namespace path="src">Moko\Component\MokoSuiteClient</namespace>
@@ -74,10 +74,6 @@
<folder>tmpl</folder> <folder>tmpl</folder>
</files> </files>
<install>
<sql><file driver="mysql" charset="utf8">admin/sql/install.mysql.sql</file></sql>
</install>
<api> <api>
<files folder="api"> <files folder="api">
<folder>src</folder> <folder>src</folder>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description> <description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCache</namespace> <namespace path="src">Moko\Module\MokoSuiteClientCache</namespace>
@@ -1,4 +1,13 @@
<?php <?php
/**
* @package MokoSuiteClient
* @subpackage mod_mokosuiteclient_cache
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Module\MokoSuiteClientCache\Administrator\Dispatcher; namespace Moko\Module\MokoSuiteClientCache\Administrator\Dispatcher;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* MokoSuiteClient Cache & Temp Cleaner — status bar split button * MokoSuiteClient Cache & Temp Cleaner — status bar with Clean dropdown
* *
* 4 buttons: Frontend link | Support PIN | Clear Cache | Clear Temp * Buttons: Frontend link | Support PIN | Clean ▾ (Cache / Temp)
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -34,12 +34,23 @@ $frontendUrl = $frontendUrl ?? '';
} }
echo $pinHtml; echo $pinHtml;
endif; ?> endif; ?>
<a href="#" class="btn btn-sm btn-outline-primary <?php echo ($pinAvailable || $frontendUrl) ? 'rounded-0 border-end-0' : 'rounded-0 rounded-start border-end-0'; ?> d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache" style="font-size:0.8rem;"> <div class="btn-group">
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache <button type="button" class="btn btn-sm btn-outline-secondary <?php echo ($pinAvailable || $frontendUrl) ? 'rounded-0 rounded-end' : 'rounded'; ?> d-flex align-items-center gap-1 px-3 py-2 dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" style="font-size:0.8rem;" id="mokosuite-clean-dropdown">
</a> <span class="icon-broom" aria-hidden="true"></span> Clean
<a href="#" class="btn btn-sm btn-outline-danger rounded-0 rounded-end d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-temp" title="Clear temp directory" style="font-size:0.8rem;"> </button>
<span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Temp <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="mokosuite-clean-dropdown">
</a> <li>
<a href="#" class="dropdown-item d-flex align-items-center gap-2" id="mokosuiteclient-clear-cache">
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Clear Cache
</a>
</li>
<li>
<a href="#" class="dropdown-item d-flex align-items-center gap-2" id="mokosuiteclient-clear-temp">
<span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Clear Temp
</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
@@ -94,7 +105,6 @@ document.addEventListener('DOMContentLoaded', function() {
setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '<?php echo $cacheUrl; ?>', '<?php echo $token; ?>'); setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '<?php echo $cacheUrl; ?>', '<?php echo $token; ?>');
setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '<?php echo $tempUrl; ?>', '<?php echo $token; ?>'); setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '<?php echo $tempUrl; ?>', '<?php echo $token; ?>');
}); });
</script> </script>
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?> <?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description> <description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace> <namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description> <description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCpanel</namespace> <namespace path="src">Moko\Module\MokoSuiteClientCpanel</namespace>
@@ -1,9 +1,6 @@
<?php <?php
/** /**
* @package MokoSuiteClient * MokoSuiteClient Cpanel — slim bar with dropdown detail panel
* @subpackage mod_mokosuiteclient_cpanel
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -13,35 +10,21 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session; use Joomla\CMS\Session\Session;
// Hidden when on MokoSuiteClient dashboard (redundant info)
if (!empty($hidden)) return; if (!empty($hidden)) return;
$siteInfo = $siteInfo ?? (object) []; $siteInfo = $siteInfo ?? (object) [];
$plugins = $plugins ?? []; $plugins = $plugins ?? [];
$healthOk = $healthOk ?? true; $healthOk = $healthOk ?? true;
$counts = $counts ?? (object) ['articles' => 0, 'users' => 0, 'extensions' => 0, 'updates' => 0]; $counts = $counts ?? (object) ['articles' => 0, 'users' => 0, 'extensions' => 0, 'updates' => 0];
$disk = $disk ?? (object) ['free_mb' => null, 'total_mb' => null]; $disk = $disk ?? (object) ['free_mb' => null, 'total_mb' => null];
$currentIp = $currentIp ?? ''; $currentIp = $currentIp ?? '';
$collapsed = true; $token = Session::getFormToken();
$showHealth = $params->get('show_health', 1); $showPlugins = $params->get('show_plugins', 1);
$showStats = $params->get('show_stats', 1);
$showDisk = $params->get('show_disk', 1);
$showIp = $params->get('show_ip', 1);
$showPlugins = $params->get('show_plugins', 1);
$showActions = $params->get('show_actions', 1);
$showVersions = $params->get('show_versions', 1);
$token = Session::getFormToken();
$enabledCount = 0; $enabledCount = 0;
$totalCount = count($plugins); $totalCount = count($plugins);
foreach ($plugins as $p) {
foreach ($plugins as $p) if ($p->enabled) $enabledCount++;
{
if ($p->enabled)
{
$enabledCount++;
}
} }
$labels = [ $labels = [
@@ -52,41 +35,106 @@ $labels = [
'mokosuiteclient_offline' => 'Offline Bypass', 'mokosuiteclient_offline' => 'Offline Bypass',
'mokosuiteclient_dbip' => 'GeoIP Lookup', 'mokosuiteclient_dbip' => 'GeoIP Lookup',
'mokosuiteclient_license' => 'License Manager', 'mokosuiteclient_license' => 'License Manager',
'mokosuiteclient_backup' => 'Backup Bridge',
]; ];
$diskPct = ($disk->total_mb && $disk->total_mb > 0) $diskPct = ($disk->total_mb && $disk->total_mb > 0)
? round((($disk->total_mb - ($disk->free_mb ?? 0)) / $disk->total_mb) * 100) ? round((($disk->total_mb - ($disk->free_mb ?? 0)) / $disk->total_mb) * 100) : null;
: null; $diskColor = ($diskPct !== null && $diskPct > 90) ? 'danger' : (($diskPct !== null && $diskPct > 75) ? 'warning' : 'success');
$diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !== null && $diskPct > 75) ? 'bg-warning' : 'bg-success');
$canDashboard = Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuiteclient');
$siteName = htmlspecialchars($siteInfo->sitename ?? '', ENT_QUOTES, 'UTF-8');
$mokoVer = htmlspecialchars($siteInfo->mokosuiteclient_version ?? '', ENT_QUOTES, 'UTF-8');
$joomlaVer = htmlspecialchars($siteInfo->joomla_version ?? '', ENT_QUOTES, 'UTF-8');
$phpVer = htmlspecialchars($siteInfo->php_version ?? '', ENT_QUOTES, 'UTF-8');
$dbType = htmlspecialchars($siteInfo->db_type ?? '', ENT_QUOTES, 'UTF-8');
$ipEscaped = htmlspecialchars($currentIp, ENT_QUOTES, 'UTF-8');
$statusDots = [];
if (!empty($siteInfo->debug)) $statusDots[] = '<span class="badge bg-warning text-dark" style="font-size:0.7rem">Debug</span>';
if (!empty($siteInfo->offline)) $statusDots[] = '<span class="badge bg-danger" style="font-size:0.7rem">Offline</span>';
if (($counts->updates ?? 0) > 0) $statusDots[] = '<span class="badge bg-info" style="font-size:0.7rem">' . (int)$counts->updates . ' updates</span>';
?> ?>
<div class="mod-mokosuiteclient-cpanel card p-3 mb-4"> <div class="mod-mokosuiteclient-cpanel mb-2" style="font-size:0.82rem;">
<div class="d-flex flex-wrap align-items-center gap-2" style="font-size:0.85rem;"> <div class="d-flex align-items-center gap-2 px-3 py-1 rounded" style="background:linear-gradient(135deg,#f8f9fa 0%,#e9ecef 100%);border:1px solid #dee2e6;">
<?php $canDashboard = Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuiteclient'); ?>
<?php if ($canDashboard): ?> <?php if ($canDashboard): ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" style="color:#1a2744;text-decoration:none;" title="MokoSuite Dashboard"><span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem"></span></a> <a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" style="color:#1a2744;text-decoration:none;" title="MokoSuite Dashboard">
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1rem"></span>
</a>
<?php else: ?> <?php else: ?>
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span> <span class="icon-shield-alt" aria-hidden="true" style="font-size:1rem;color:#1a2744"></span>
<?php endif; ?> <?php endif; ?>
<span class="fw-bold"><?php echo htmlspecialchars($siteInfo->sitename ?? ''); ?></span>
<span class="badge bg-primary">MokoSuite <?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span> <span class="fw-semibold"><?php echo $siteName; ?></span>
<span class="text-muted" style="font-size:0.75rem">v<?php echo $mokoVer; ?></span>
<?php echo implode(' ', $statusDots); ?>
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderBadge( <?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderBadge(
['available' => !empty($supportPinAvailable), 'pin' => $supportPin ?? ''], ['available' => !empty($supportPinAvailable), 'pin' => $supportPin ?? ''],
$token, 'cpanel' $token, 'cpanel'
); ?> ); ?>
<span class="badge bg-secondary">Joomla <?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?></span>
<span class="badge bg-secondary">PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<span class="badge bg-secondary"><?php echo htmlspecialchars($siteInfo->db_type ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<span class="ms-auto d-flex align-items-center gap-2"> <span class="ms-auto d-flex align-items-center gap-2">
<span class="icon-globe" aria-hidden="true"></span> <span class="text-muted" style="font-size:0.75rem">
<code><?php echo htmlspecialchars($currentIp); ?></code> <span class="icon-globe" aria-hidden="true"></span> <code style="font-size:0.75rem"><?php echo $ipEscaped; ?></code>
</span>
<button class="btn btn-sm btn-link text-muted p-0 ms-1" type="button" data-bs-toggle="collapse" data-bs-target="#mokosuite-cpanel-detail" aria-expanded="false" aria-controls="mokosuite-cpanel-detail" title="Show details" style="font-size:0.85rem;text-decoration:none;">
<span class="icon-chevron-down mokosuite-cpanel-chevron" aria-hidden="true" style="transition:transform 0.2s"></span>
</button>
</span> </span>
</div> </div>
<div class="collapse" id="mokosuite-cpanel-detail">
<div class="card card-body mt-1 p-3" style="font-size:0.82rem;border-color:#dee2e6;">
<div class="row g-3">
<div class="col-md-4">
<h6 class="text-muted mb-2" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em">Environment</h6>
<div class="d-flex flex-wrap gap-1">
<span class="badge bg-primary">MokoSuite <?php echo $mokoVer; ?></span>
<span class="badge bg-secondary">Joomla <?php echo $joomlaVer; ?></span>
<span class="badge bg-secondary">PHP <?php echo $phpVer; ?></span>
<span class="badge bg-secondary"><?php echo $dbType; ?></span>
</div>
</div>
<div class="col-md-4">
<h6 class="text-muted mb-2" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em">Stats</h6>
<div class="d-flex flex-wrap gap-2" style="font-size:0.8rem">
<span><span class="icon-file-alt" aria-hidden="true"></span> <?php echo (int)($counts->articles ?? 0); ?> articles</span>
<span><span class="icon-users" aria-hidden="true"></span> <?php echo (int)($counts->users ?? 0); ?> users</span>
<span><span class="icon-puzzle-piece" aria-hidden="true"></span> <?php echo (int)($counts->extensions ?? 0); ?> extensions</span>
</div>
<?php if ($diskPct !== null): ?>
<div class="mt-1">
<div class="progress" style="height:4px">
<div class="progress-bar bg-<?php echo $diskColor; ?>" style="width:<?php echo $diskPct; ?>%"></div>
</div>
<small class="text-muted">Disk <?php echo $diskPct; ?>% (<?php echo number_format(($disk->free_mb ?? 0) / 1024, 1); ?> GB free)</small>
</div>
<?php endif; ?>
</div>
<?php if ($showPlugins && $totalCount > 0): ?>
<div class="col-md-4">
<h6 class="text-muted mb-2" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em">Plugins (<?php echo $enabledCount; ?>/<?php echo $totalCount; ?>)</h6>
<div class="d-flex flex-wrap gap-1">
<?php foreach ($plugins as $p):
$el = str_replace('plg_system_', '', $p->element);
$label = $labels[$el] ?? ucfirst(str_replace('mokosuiteclient_', '', $el));
$color = $p->enabled ? 'success' : 'secondary';
?>
<span class="badge bg-<?php echo $color; ?>" style="font-size:0.7rem"><?php echo htmlspecialchars($label); ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div> </div>
<style>
[aria-expanded="true"] .mokosuite-cpanel-chevron { transform: rotate(180deg); }
</style>
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?> <?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description> <description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace> <namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace>
@@ -2,9 +2,8 @@
/** /**
* MokoSuiteClient Admin Sidebar Menu * MokoSuiteClient Admin Sidebar Menu
* *
* Each installed Moko component gets its own top-level collapsible section. * Single "MokoSuite" top-level item with all Moko ecosystem components
* com_mokosuitehq is always pinned first. com_mokosuiteclient uses static views * as collapsible children underneath.
* as children. All other components auto-discover their submenu items.
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -29,6 +28,7 @@ $allViews = [
['icon' => 'fa-solid fa-file-lines', 'title' => 'Templates', 'link' => 'index.php?option=com_mokosuiteclient&view=templates', 'acl' => 'mokosuiteclient.templates.manage'], ['icon' => 'fa-solid fa-file-lines', 'title' => 'Templates', 'link' => 'index.php?option=com_mokosuiteclient&view=templates', 'acl' => 'mokosuiteclient.templates.manage'],
['icon' => 'fa-solid fa-right-left', 'title' => 'Replacements', 'link' => 'index.php?option=com_mokosuiteclient&view=replacements','acl' => 'mokosuiteclient.replacements.manage'], ['icon' => 'fa-solid fa-right-left', 'title' => 'Replacements', 'link' => 'index.php?option=com_mokosuiteclient&view=replacements','acl' => 'mokosuiteclient.replacements.manage'],
['icon' => 'fa-solid fa-shuffle', 'title' => 'Conditions', 'link' => 'index.php?option=com_mokosuiteclient&view=conditions', 'acl' => 'mokosuiteclient.conditions.manage'], ['icon' => 'fa-solid fa-shuffle', 'title' => 'Conditions', 'link' => 'index.php?option=com_mokosuiteclient&view=conditions', 'acl' => 'mokosuiteclient.conditions.manage'],
['icon' => 'icon-cube', 'title' => 'Modules', 'link' => 'index.php?option=com_mokosuiteclient&view=modules', 'acl' => 'core.admin'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database', 'acl' => 'core.admin'], ['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database', 'acl' => 'core.admin'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup', 'acl' => 'mokosuiteclient.cache'], ['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup', 'acl' => 'mokosuiteclient.cache'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient', 'acl' => 'core.admin'], ['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient', 'acl' => 'core.admin'],
@@ -38,6 +38,72 @@ $mokosuiteclientStaticViews = array_filter($allViews, function ($v) use ($user,
return $isSuper || $user->authorise($v['acl'], 'com_mokosuiteclient'); return $isSuper || $user->authorise($v['acl'], 'com_mokosuiteclient');
}); });
// ── Icon overrides (canonical icons per component) ───────────────────
$iconOverrides = [
'com_mokosuiteclient' => 'icon-shield-alt',
'com_mokosuitehq' => 'icon-tachometer-alt',
'com_mokosuitebackup' => 'icon-archive',
'com_mokosuitecrm' => 'icon-address-book',
'com_mokosuiteerp' => 'icon-briefcase',
'com_mokosuiteshop' => 'icon-shopping-cart',
'com_mokosuitepos' => 'icon-calculator',
'com_mokosuitemrp' => 'icon-cogs',
'com_mokosuitehrm' => 'icon-id-badge',
'com_mokosuiterestaurant' => 'icon-utensils',
'com_mokosuitechild' => 'icon-child',
'com_mokosuitenpo' => 'icon-heart',
'com_mokosuitefield' => 'icon-wrench',
'com_mokosuitecreate' => 'icon-paint-brush',
'com_mokosuiteforms' => 'icon-list-alt',
'com_mokosuitecommunity' => 'icon-comments',
'com_mokosuitecross' => 'icon-share-alt',
'com_mokosuiteopengraph' => 'icon-globe',
'com_mokosuitestorelocator' => 'icon-map-marker-alt',
];
$childIconMap = [
'dashboard' => 'icon-tachometer-alt',
'contacts' => 'icon-address-book',
'deals' => 'icon-handshake',
'activities' => 'icon-clock',
'tickets' => 'icon-life-ring',
'helpdesk' => 'icon-life-ring',
'products' => 'icon-box',
'orders' => 'icon-shopping-cart',
'invoices' => 'icon-file-invoice',
'inventory' => 'icon-warehouse',
'settings' => 'icon-cog',
'config' => 'icon-cog',
'options' => 'icon-sliders-h',
'reports' => 'icon-chart-bar',
'analytics' => 'icon-chart-line',
'locations' => 'icon-map-marker-alt',
'stores' => 'icon-store',
'categories' => 'icon-folder',
'forms' => 'icon-list-alt',
'submissions' => 'icon-inbox',
'emails' => 'icon-envelope',
'campaigns' => 'icon-bullhorn',
'users' => 'icon-users',
'members' => 'icon-id-card',
'employees' => 'icon-id-badge',
'donors' => 'icon-hand-holding-heart',
'donations' => 'icon-donate',
'events' => 'icon-calendar',
'tasks' => 'icon-tasks',
'projects' => 'icon-project-diagram',
'templates' => 'icon-file-alt',
'announcements'=> 'icon-bullhorn',
'plugins' => 'icon-plug',
'import' => 'icon-upload',
'export' => 'icon-download',
'log' => 'icon-list',
'backup' => 'icon-archive',
'channels' => 'icon-share-alt',
'posts' => 'icon-paper-plane',
'schedule' => 'icon-calendar-alt',
];
// ── Auto-discover all Moko components from #__menu ────────────────── // ── Auto-discover all Moko components from #__menu ──────────────────
$mokoComponents = []; $mokoComponents = [];
@@ -56,7 +122,6 @@ try
); );
$menuItems = $db->loadObjectList() ?: []; $menuItems = $db->loadObjectList() ?: [];
// Load language files for discovered components
$lang = Factory::getLanguage(); $lang = Factory::getLanguage();
$loadedLangs = []; $loadedLangs = [];
foreach ($menuItems as $m) foreach ($menuItems as $m)
@@ -65,47 +130,50 @@ try
{ {
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR); $lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
$lang->load($m->element, JPATH_ADMINISTRATOR); $lang->load($m->element, JPATH_ADMINISTRATOR);
// Also try component-local language path (Joomla 5/6 pattern)
$compLangPath = JPATH_ADMINISTRATOR . '/components/' . $m->element; $compLangPath = JPATH_ADMINISTRATOR . '/components/' . $m->element;
if (is_dir($compLangPath . '/language')) if (is_dir($compLangPath . '/language'))
{ {
$lang->load($m->element . '.sys', $compLangPath); $lang->load($m->element . '.sys', $compLangPath);
$lang->load($m->element, $compLangPath); $lang->load($m->element, $compLangPath);
} }
$loadedLangs[$m->element] = true; $loadedLangs[$m->element] = true;
} }
} }
// Group: level 1 = component parent, level 2 = children
foreach ($menuItems as $m) foreach ($menuItems as $m)
{ {
if ((int) $m->level === 1) if ((int) $m->level === 1)
{ {
$icon = $iconOverrides[$m->element]
?? str_replace('class:', 'icon-', $m->img ?: 'class:puzzle-piece');
$mokoComponents[$m->element] = [ $mokoComponents[$m->element] = [
'id' => $m->id, 'id' => $m->id,
'title' => Text::_($m->title), 'title' => Text::_($m->title),
'link' => $m->link, 'link' => $m->link,
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:puzzle-piece'), 'icon' => $icon,
'element' => $m->element, 'element' => $m->element,
'children' => [], 'children' => [],
]; ];
} }
elseif ((int) $m->level === 2 && isset($mokoComponents[$m->element])) elseif ((int) $m->level === 2 && isset($mokoComponents[$m->element]))
{ {
$childIcon = str_replace('class:', 'icon-', $m->img ?: '');
if ($childIcon === '' || $childIcon === 'icon-cog' || $childIcon === 'icon-cogs')
{
$parsed = [];
parse_str(parse_url($m->link, PHP_URL_QUERY) ?? '', $parsed);
$viewKey = strtolower($parsed['view'] ?? '');
$childIcon = $childIconMap[$viewKey] ?? '';
}
$mokoComponents[$m->element]['children'][] = [ $mokoComponents[$m->element]['children'][] = [
'title' => Text::_($m->title), 'title' => Text::_($m->title),
'link' => $m->link, 'link' => $m->link,
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:cog'), 'icon' => $childIcon ?: 'icon-angle-right',
]; ];
} }
} }
} }
catch (\Throwable $e) catch (\Throwable $e) {}
{
// Silent — menu works without auto-discovered components
}
// Override com_mokosuiteclient children with static views // Override com_mokosuiteclient children with static views
if (isset($mokoComponents['com_mokosuiteclient'])) if (isset($mokoComponents['com_mokosuiteclient']))
@@ -115,7 +183,6 @@ if (isset($mokoComponents['com_mokosuiteclient']))
} }
else else
{ {
// com_mokosuiteclient not in admin menu — add it manually
$mokoComponents['com_mokosuiteclient'] = [ $mokoComponents['com_mokosuiteclient'] = [
'id' => 0, 'id' => 0,
'title' => 'MokoSuite', 'title' => 'MokoSuite',
@@ -133,9 +200,6 @@ $rest = [];
foreach ($mokoComponents as $key => $comp) foreach ($mokoComponents as $key => $comp)
{ {
// Shorten display titles:
// MokoSuiteClient → MokoSuite, MokoSuiteHQ → MokoHQ
// Everything else: MokoSuiteBackup → Backup, MokoSuiteOpenGraph → OpenGraph
if ($key === 'com_mokosuiteclient') if ($key === 'com_mokosuiteclient')
{ {
$comp['title'] = 'MokoSuite'; $comp['title'] = 'MokoSuite';
@@ -149,35 +213,29 @@ foreach ($mokoComponents as $key => $comp)
$comp['title'] = preg_replace('/^MokoSuite\s*/i', '', $comp['title']); $comp['title'] = preg_replace('/^MokoSuite\s*/i', '', $comp['title']);
} }
if ($key === 'com_mokosuitehq') if ($key === 'com_mokosuitehq') { $hq = $comp; }
{ elseif ($key === 'com_mokosuiteclient') { $client = $comp; }
$hq = $comp; else { $rest[$key] = $comp; }
}
elseif ($key === 'com_mokosuiteclient')
{
$client = $comp;
}
else
{
$rest[$key] = $comp;
}
} }
usort($rest, fn($a, $b) => strcasecmp($a['title'], $b['title'])); usort($rest, fn($a, $b) => strcasecmp($a['title'], $b['title']));
$sorted = []; $sorted = [];
if ($hq !== null) if ($hq !== null) $sorted[] = $hq;
if ($client !== null) $sorted[] = $client;
foreach ($rest as $comp) $sorted[] = $comp;
// Is ANY Moko component active?
$anyActive = false;
foreach ($sorted as $comp)
{ {
$sorted[] = $hq; $p = [];
} parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $p);
if ($client !== null) if (($p['option'] ?? '') === $currentOption) { $anyActive = true; break; }
{
$sorted[] = $client;
}
foreach ($rest as $comp)
{
$sorted[] = $comp;
} }
if ($currentOption === 'com_plugins') $anyActive = true;
$iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;';
?> ?>
<style> <style>
@@ -185,58 +243,64 @@ foreach ($rest as $comp)
</style> </style>
<ul class="nav flex-column main-nav"> <ul class="nav flex-column main-nav">
<?php foreach ($sorted as $comp): ?> <li class="item parent item-level-1 mokosuiteclient-ext-item<?php echo $anyActive ? ' mm-active' : ''; ?>">
<?php <a class="has-arrow<?php echo $anyActive ? ' mm-active' : ''; ?>" href="#">
$compParsed = []; <span class="icon-shield-alt" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed); <span class="sidebar-item-title">MokoSuite</span>
$compOption = $compParsed['option'] ?? '';
$compActive = ($compOption === $currentOption);
// For com_mokosuiteclient static children, also check the plugins filter link
if (!$compActive && $comp['element'] === 'com_mokosuiteclient' && $currentOption === 'com_plugins')
{
$compActive = true;
}
$hasChildren = !empty($comp['children']);
$liClass = 'item mokosuiteclient-ext-item' . ($hasChildren ? ' parent item-level-1' : '') . ($compActive ? ' mm-active' : '');
$aClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
$childCollapse = 'collapse-level-1 mm-collapse' . ($compActive ? ' mm-show' : '');
?>
<li class="<?php echo $liClass; ?>">
<a class="<?php echo $aClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
</a> </a>
<?php if ($hasChildren): ?> <ul class="collapse-level-1 mm-collapse<?php echo $anyActive ? ' mm-show' : ''; ?>" style="padding-inline-start:0.5rem;">
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.5rem;"> <?php foreach ($sorted as $comp): ?>
<?php foreach ($comp['children'] as $child): ?>
<?php <?php
$childParsed = []; $compParsed = [];
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed); parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
$childOption = $childParsed['option'] ?? ''; $compOption = $compParsed['option'] ?? '';
$childView = $childParsed['view'] ?? ''; $compActive = ($compOption === $currentOption);
if (!$compActive && $comp['element'] === 'com_mokosuiteclient' && $currentOption === 'com_plugins')
$childActive = false;
if ($childOption === $currentOption)
{ {
$childActive = empty($childView) $compActive = true;
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === $childView);
} }
$childLiClass = 'item mokosuiteclient-ext-child' . ($childActive ? ' mm-active' : ''); $hasChildren = !empty($comp['children']);
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
?> ?>
<li class="<?php echo $childLiClass; ?>"> <?php if ($hasChildren): ?>
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>> <li class="item parent item-level-2 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span> <a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#">
<span class="sidebar-item-title"><?php echo $child['title']; ?></span> <span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
</a>
<ul class="collapse-level-2 mm-collapse<?php echo $compActive ? ' mm-show' : ''; ?>" style="padding-inline-start:0.5rem;">
<?php foreach ($comp['children'] as $child): ?>
<?php
$childParsed = [];
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
$childOption = $childParsed['option'] ?? '';
$childView = $childParsed['view'] ?? '';
$childActive = false;
if ($childOption === $currentOption)
{
$childActive = empty($childView)
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === $childView);
}
?>
<li class="item mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $childActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo htmlspecialchars($child['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8'); ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</li>
<?php else: ?>
<li class="item mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $compActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($comp['link']); ?>"<?php echo $compActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
</a> </a>
</li> </li>
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?>
</li> </li>
<?php endforeach; ?>
</ul> </ul>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient * REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.16 * VERSION: 02.51.03
* PATH: /src/Extension/MokoSuiteClient.php * PATH: /src/Extension/MokoSuiteClient.php
* NOTE: Core system plugin for MokoSuiteClient admin tools suite * NOTE: Core system plugin for MokoSuiteClient admin tools suite
*/ */
@@ -384,14 +384,20 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) { document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) {
link.href = url; link.href = url;
link.target = '_blank'; link.target = '_blank';
link.rel = 'noopener noreferrer';
var extIcon = link.querySelector('.icon-external-link-alt, .icon-external-link, .fa-external-link-alt, .fa-up-right-from-square');
if (extIcon) extIcon.remove();
}); });
document.querySelectorAll('a[href*=\"dashboard=help\"]').forEach(function(link) { document.querySelectorAll('a[href*=\"dashboard=help\"]').forEach(function(link) {
link.href = url; link.href = url;
link.target = '_blank'; link.target = '_blank';
link.rel = 'noopener noreferrer'; link.rel = 'noopener noreferrer';
var extIcon = link.querySelector('.icon-external-link-alt, .icon-external-link, .fa-external-link-alt, .fa-up-right-from-square');
if (extIcon) extIcon.remove();
}); });
}); });
"); ");
$doc->addStyleDeclaration('a[href=\"' . $supportUrl . '\"] .icon-external-link-alt, a[href=\"' . $supportUrl . '\"] .fa-up-right-from-square { display: none !important; }');
} }
/** /**
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* VERSION: 02.48.16 * VERSION: 02.51.03
* PATH: /src/Field/ArticlesField.php * PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles * BRIEF: List field that populates with published Joomla articles
*/ */
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* VERSION: 02.48.16 * VERSION: 02.51.03
* PATH: /src/Field/CopyableTokenField.php * PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button * BRIEF: Read-only token field with a copy-to-clipboard button
*/ */
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license> <license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description> <description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace> <namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient * REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.16 * VERSION: 02.51.03
* PATH: /src/script.php * PATH: /src/script.php
* BRIEF: Installation script for MokoSuiteClient plugin * BRIEF: Installation script for MokoSuiteClient plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment * NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient * REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.16 * VERSION: 02.51.03
* PATH: /src/services/provider.php * PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x * BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container * NOTE: Registers the plugin with Joomla's DI container
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
@@ -23,6 +23,7 @@ class DBIP extends CMSPlugin implements SubscriberInterface
{ {
return [ return [
'onAfterInitialise' => 'onAfterInitialise', 'onAfterInitialise' => 'onAfterInitialise',
'onAfterRender' => 'onAfterRender',
]; ];
} }
@@ -80,4 +81,92 @@ class DBIP extends CMSPlugin implements SubscriberInterface
DBIPHelper::downloadCityDb($url); DBIPHelper::downloadCityDb($url);
} }
/**
* Scan rendered admin HTML for IP addresses and enrich with geo flags + tooltips.
*/
public function onAfterRender(): void
{
$app = $this->getApplication();
if (!$app->isClient('administrator'))
{
return;
}
if ($app->getDocument()->getType() !== 'html')
{
return;
}
$body = $app->getBody();
if (empty($body))
{
return;
}
$ipv4 = '(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)';
$ipv6 = '(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))?';
$pattern = '#<code(?:\s[^>]*)?>(' . $ipv4 . '|' . $ipv6 . ')</code>#';
$cache = [];
$newBody = preg_replace_callback($pattern, function ($m) use (&$cache) {
$fullMatch = $m[0];
$ip = $m[1];
// Skip private/loopback
if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE) === false)
{
return $fullMatch;
}
// Already enriched (has a title attribute)
if (strpos($fullMatch, 'title=') !== false)
{
return $fullMatch;
}
if (!isset($cache[$ip]))
{
$cache[$ip] = DBIPHelper::lookup($ip);
}
$geo = $cache[$ip];
if ($geo === null || empty($geo['country_code']))
{
return $fullMatch;
}
$cc = strtoupper($geo['country_code']);
$flag = self::countryFlag($cc);
$parts = array_filter([$geo['city'], $geo['region'], $geo['country_name']]);
$tooltip = htmlspecialchars(implode(', ', $parts), \ENT_QUOTES, 'UTF-8');
$escaped = htmlspecialchars($ip, \ENT_QUOTES, 'UTF-8');
return $flag . ' <code title="' . $tooltip . '" style="cursor:help;">' . $escaped . '</code>';
}, $body);
if ($newBody !== null && $newBody !== $body)
{
$app->setBody($newBody);
}
}
private static function countryFlag(string $cc): string
{
if (\strlen($cc) !== 2)
{
return '';
}
$cc = strtoupper($cc);
$first = mb_chr(0x1F1E6 + \ord($cc[0]) - \ord('A'));
$second = mb_chr(0x1F1E6 + \ord($cc[1]) - \ord('A'));
return $first . $second;
}
} }
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace>
<files><folder>src</folder><folder>services</folder><folder>language</folder></files> <files><folder>src</folder><folder>services</folder><folder>language</folder></files>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description> <description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license> <license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description> <description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace> <namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php * PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php
* VERSION: 02.48.16 * VERSION: 02.51.03
* BRIEF: Content-only snapshot/restore for demo site reset * BRIEF: Content-only snapshot/restore for demo site reset
*/ */
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license> <license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description> <description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace> <namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php * PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php
* VERSION: 02.48.16 * VERSION: 02.51.03
* BRIEF: Receiver-side content sync — applies incoming payload to local DB * BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/ */
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php * PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php
* VERSION: 02.48.16 * VERSION: 02.51.03
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites * BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/ */
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.16</version> <version>02.51.03</version>
<description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description> <description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace> <namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace>
<files> <files>
+2 -2
View File
@@ -2,14 +2,14 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoSuiteClient</name> <name>Package - MokoSuiteClient</name>
<packagename>mokosuiteclient</packagename> <packagename>mokosuiteclient</packagename>
<version>02.48.16</version> <version>02.51.03</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright> <copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license> <license>GNU General Public License version 3 or later; see LICENSE</license>
<description>MokoSuiteClient site management suite admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API.</description> <description>MokoSuiteClient site management suite: admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API.</description>
<dlid prefix="dlid=" suffix=""/> <dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall> <blockChildUninstall>true</blockChildUninstall>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>