Compare commits

...

40 Commits

Author SHA1 Message Date
gitea-actions[bot] b380c49a5e chore: promote changelog [Unreleased] → [02.52.13]
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
2026-06-29 16:35:40 +00:00
gitea-actions[bot] 5aaa8be394 chore: update channels for 02.52.13 [skip ci] 2026-06-29 16:35:39 +00:00
gitea-actions[bot] dc1f114d93 chore(release): build 02.52.13 [skip ci] 2026-06-29 16:35:33 +00:00
jmiller 15510cfa1a Merge pull request 'fix: promote dev features to main' (#279) from fix/promote-dev-features into main
Merge PR #279: promote dev features to main
2026-06-29 16:35:23 +00:00
gitea-actions[bot] c452077269 chore(version): pre-release bump to 02.52.13-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 19s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3m0s
2026-06-29 16:34:28 +00:00
jmiller ce1541208c fix: add updates.xml and remove dead update server migration code
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
Add updates.xml to repo root for Joomla update checker.
Remove unused migrateUpdateServerUrls(), fixUpdateRecords(), and
cleanupStaleUpdateSites() methods from install script.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:32:57 -05:00
jmiller 6c668dae20 fix: address PR review findings - CSRF response, N+1 query, error handling
- togglePublished: return JSON on CSRF failure instead of die()
- Conditions view: fold group/rule counts into main query as subselects
  instead of N+1 per-item queries
- All 5 toggle-published templates: add .catch() for AJAX error feedback

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:32:27 -05:00
jmiller 3ecdbacbeb feat: add missing submenu entries and fix menu module icon overrides
- Add 6 submenu items to manifest (Conditions, Snippets, Templates,
  Replacements, Automation, Modules) so views are navigable
- Fix icon overrides for actual element names (com_mokosuite_crm,
  com_mokosuite_erp, com_mokoog, com_mokoshop)
- Add icon mappings for 11 additional MokoSuite components
- Fix item-level CSS classes to match Joomla admin sidebar

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:32:26 -05:00
jmiller d0db1f55dc fix: heartbeat button shows proper errors instead of failing silently
- CSRF check returns JSON instead of die() with raw text
- JS parses non-JSON responses gracefully and shows server error
- Visual feedback (check/cross icon) on success/failure
- 3-second icon revert after result

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:32:25 -05:00
jmiller 6a717342db feat: add Conditions, Snippets, Replacements, Templates, Modules views
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-29 11:30:36 -05:00
jmiller aa52076cb0 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-29 15:44:16 +00:00
gitea-actions[bot] 886e36f4f4 chore: promote changelog [Unreleased] → [02.52.04] 2026-06-29 15:43:45 +00:00
gitea-actions[bot] 7f1e99c818 chore(release): build 02.52.04 [skip ci] 2026-06-29 15:43:35 +00:00
jmiller 8e564b1fca Merge pull request 'fix: sync schema version and add missing license_cache migration' (#275) from fix/schema-version-sync into main 2026-06-29 15:43:16 +00:00
gitea-actions[bot] dea991a3b2 chore(version): pre-release bump to 02.52.04-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 35s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 4m21s
2026-06-29 15:17:28 +00:00
gitea-actions[bot] 54e61ded92 chore(version): pre-release bump to 02.52.03-dev [skip ci] 2026-06-29 15:16:54 +00:00
jmiller e746afd835 fix: add missing SQL migration for license_cache table and sync schema version
Universal: PR Check / Branch Policy (pull_request) Failing after 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 21s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m3s
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
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: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
The mokosuite_license_cache table was added to install.mysql.sql but
never had a corresponding update file. Existing installs (upgrading
from pre-02.48.20) never received this table.

Additionally, no SQL update files existed between 02.48.20 and 02.52.00,
causing Joomla to report a database schema mismatch (manifest says
02.52.00 but #__schemas is stuck at 02.48.20).
2026-06-29 15:16:25 +00:00
gitea-actions[bot] 31a049141f chore(version): pre-release bump to 02.52.02-dev [skip ci]
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
2026-06-29 15:16:25 +00:00
gitea-actions[bot] 67e9cc6b38 chore: promote changelog [Unreleased] → [02.52.00]
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
2026-06-29 15:11:00 +00:00
gitea-actions[bot] 5c8192dd0d chore(release): build 02.52.00 [skip ci] 2026-06-29 15:10:49 +00:00
jmiller eb99bae991 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-29 15:10:14 +00:00
gitea-actions[bot] 6d56ec550a chore: promote changelog [Unreleased] → [02.51.10] 2026-06-29 15:09:39 +00:00
gitea-actions[bot] 44c653e09b chore(release): build 02.51.10 [skip ci] 2026-06-29 15:09:26 +00:00
jmiller 4718ebf32d Merge pull request 'fix: correct menu item-level classes to match Joomla admin sidebar' (#273) from fix/menu-item-levels into main
fix: correct menu item-level classes to match Joomla admin sidebar (#273)
2026-06-29 15:09:05 +00:00
gitea-actions[bot] af193fc2bb chore(version): pre-release bump to 02.51.10-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 39s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 7m12s
2026-06-29 15:08:36 +00:00
jmiller 4850928e6d fix: correct menu item-level classes to match Joomla admin sidebar
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 12s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 18s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Build & Release / Promote to RC (pull_request) Failing after 15s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
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: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Components use item-level-1, sub-items use item-level-2 to align
with Atum template's native sidebar indentation and styling.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 10:08:07 -05:00
jmiller ff1ee76d71 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-29 06:34:21 +00:00
jmiller 99caa708b8 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 19:55:01 +00:00
gitea-actions[bot] fd65148f38 chore: promote changelog [Unreleased] → [02.51.08] 2026-06-28 19:53:56 +00:00
gitea-actions[bot] e7b5f78deb chore(release): build 02.51.08 [skip ci] 2026-06-28 19:53:44 +00:00
jmiller 62bf4af481 Merge pull request 'fix: heartbeat button silent failure' (#270) from fix/heartbeat-error-reporting into main
fix: heartbeat button silent failure (#270)
2026-06-28 19:53:04 +00:00
jmiller f1345b0010 fix: clear pending icon timeout on rapid heartbeat re-click
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 33s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 48s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Generic: Repo Health / Site Health (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 37s
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: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 40m20s
Prevents spinner color bleed and mid-flight icon reset when
user clicks the heartbeat button again before the 3s revert.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 14:52:41 -05:00
gitea-actions[bot] 8496b66db2 chore(version): pre-release bump to 02.51.08-dev [skip ci] 2026-06-28 19:48:50 +00:00
jmiller 1155b0fa17 fix: heartbeat button shows proper errors instead of failing silently
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 10s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: PR Check / Secret Scan (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 42s
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: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
- CSRF check returns JSON instead of die() with raw text
- JS parses non-JSON responses gracefully and shows server error
- Visual feedback (check/cross icon) on success/failure
- 3-second icon revert after result

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 14:44:29 -05:00
jmiller f0a8ad6c39 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 18:34:37 +00:00
gitea-actions[bot] 5313723c68 chore(version): pre-release bump to 02.51.01-dev [skip ci] 2026-06-28 18:33:10 +00:00
jmiller a867a8fdde chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 18:30:35 +00:00
gitea-actions[bot] d2b4b70f0e chore: promote changelog [Unreleased] → [02.50.00] 2026-06-28 18:29:56 +00:00
gitea-actions[bot] 1c0fa55955 chore(release): build 02.50.00 [skip ci] 2026-06-28 18:29:46 +00:00
jmiller 9eb7ce03e6 Merge pull request 'Release: dev to main - UI cleanup, RL import, menu icons' (#269) from merge/dev-to-main-268 into main 2026-06-28 18:29:17 +00:00
66 changed files with 1625 additions and 270 deletions
+3 -1
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +=======================================================================+ # +=======================================================================+
@@ -75,6 +75,7 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools - name: Setup mokocli tools
env: env:
@@ -173,6 +174,7 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes - name: Configure git for bot pushes
run: | run: |
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: mokocli.Automation
# VERSION: 01.00.00 # VERSION: 02.52.13
# 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"
+7 -58
View File
@@ -14,72 +14,21 @@
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.52 VERSION: 02.52.13
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
### Added ## [02.52.13] --- 2026-06-29
- **Mirror Domains & Staging** — repeatable subform table in DevTools plugin for configuring domain aliases with per-alias offline bypass, robots directive, and labels
- **Daily Support PIN** — HMAC-SHA256 rotating PIN shown on cpanel module, component dashboard, and HQ site cards
- **Support PIN in status bar** — cache/temp module now shows PIN request button instead of domain; click to request, click again to copy
- **Frontend link in status bar** — cache/temp module now has 4 buttons: Site (frontend link), PIN, Cache, Temp
- **Help buttons** — all admin views link to Gitea wiki pages via toolbar help button
- **Support PIN in heartbeat** — core system plugin includes current PIN in heartbeat payload to HQ
- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
### Changed ## [02.52.13] --- 2026-06-29
- **Support PIN UI unified** — `SupportPinHelper::renderBadge()` and `renderScript()` replace 3 separate inline implementations (dashboard, cpanel module, cache module) with click-to-copy on all PIN badges
- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
- **SupportPinHelper** — shared helper centralises PIN generation across dashboard, cpanel module, cache module, and AJAX controller
- **Current IP display** — firewall plugin settings show admin's IP with copy button
- **Heartbeat monitor** — consolidated into core plugin from retired monitor plugin, with diagnostic logging on all bail-out points
- **Backup bridge plugin** — discovers MokoSuiteBackup's BackupStatusHelper and sends status in heartbeat payloads
- **Activity log** — blockchain-style hash chain for tamper detection in MokoSuiteHQ
- **Dev domain in heartbeat** — client sends dev alias to HQ for display on dashboard
- **Login app badges** — recent logins table shows Admin/Site badge parsed from Joomla action log message JSON
### Changed ## [02.52.04] --- 2026-06-29
- **Plugin install** — self-healing: extracts plugin zips from package on every update, creates missing extension records with namespace
- **Menu naming** — MokoSuiteClient displays as "MokoSuite", MokoSuiteHQ as "MokoHQ", others stripped of prefix
- **Menu ordering** — HQ first, MokoSuite second, others alphabetical
- **Cpanel module** — always starts collapsed, access level 3 (Special), pretty plugin badge labels
- **Module namespaces** — fixed cpanel (MokoSuiteCpanel → MokoSuiteClientCpanel) and cache (MokoSuiteCache → MokoSuiteClientCache)
- **Health checks** — return status:error on exceptions instead of false status:ok; MokoSuiteBackup detection queries correct table
- **Heartbeat** — correct URL (suite.dev), correct API route (mokosuitehq), correct headers (X-MokoSuite-*), fresh RSA key pair
- **Date formats** — all templates use Joomla locale-aware DATE_FORMAT_LC2/LC4
- **Domains** — updated from waas.dev to suite.dev.mokoconsulting.tech throughout
- **Dashboard info bar** — reverted stacked layout; info items back to horizontal row
- **Extension version bar** — full-width auto-sized strip with equal-width cells and border separators
- **Recent logins** — exact match on LOGGED_IN key (excludes logout noise), limit increased to 10
### Removed ## [02.52.04] --- 2026-06-29
- **Helpdesk/tickets** — migrated to MokoSuiteCRM (issue #67)
- **Monitor plugin** — retired, config consolidated into core plugin
- **Backup bridge** — temporarily removed from package manifest (build pipeline issue)
- **Update server migration** — removed migrateUpdateServerUrls, cleanupStaleUpdateSites, fixUpdateRecords, enableUpdateServer calls
### Fixed ## [02.52.00] --- 2026-06-29
- **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
- License plugin missing `src/` and `language/` directories causing install failure
- PIN generation inconsistency — controller used `floor(now/TTL)` while display used `floor(requestedAt/TTL)`
- Plugin files installing to group root instead of element subdirectory (ALTER TABLE DEFAULT '' + empty element cleanup)
- Orphan extension rows with empty element or display-name-as-element
- Module not publishing (ensureAdminModule direct DB update bypasses checked_out)
- RSA key pair had Windows line endings causing signature verification failure
- Heartbeat connection failing due to wrong domain, route, and header names
## [02.44.00] --- 2026-06-20 ## [02.52.00] --- 2026-06-29
## [02.42.00] --- 2026-06-20
## [02.42.00] --- 2026-06-20
+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.52 VERSION: 02.52.13
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.52 VERSION: 02.52.13
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.52 VERSION: 02.52.13
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.52 VERSION: 02.52.13
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.52 VERSION: 02.52.13
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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Build Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Configuration Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Installation Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Operations Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Testing Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Troubleshooting Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Documentation Index (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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.52) # MokoSuiteClient Plugin Overview (VERSION: 02.52.13)
## 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.52 VERSION: 02.52.13
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
--> -->
@@ -17,3 +17,9 @@ COM_MOKOSUITECLIENT_MENU_WAFLOG="WAF Log"
COM_MOKOSUITECLIENT_MENU_DATABASE="Database Tools" COM_MOKOSUITECLIENT_MENU_DATABASE="Database Tools"
COM_MOKOSUITECLIENT_MENU_CLEANUP="Cache Cleanup" COM_MOKOSUITECLIENT_MENU_CLEANUP="Cache Cleanup"
COM_MOKOSUITECLIENT_MENU_CACHE="Cache Management" COM_MOKOSUITECLIENT_MENU_CACHE="Cache Management"
COM_MOKOSUITECLIENT_MENU_CONDITIONS="Conditions"
COM_MOKOSUITECLIENT_MENU_SNIPPETS="Snippets"
COM_MOKOSUITECLIENT_MENU_TEMPLATES="Content Templates"
COM_MOKOSUITECLIENT_MENU_REPLACEMENTS="Replacements"
COM_MOKOSUITECLIENT_MENU_AUTOMATION="Automation"
COM_MOKOSUITECLIENT_MENU_MODULES="Modules"
@@ -0,0 +1,11 @@
-- 02.52.00 — Create license cache table (missing from prior migrations)
-- This table was in install.mysql.sql but never had an update file,
-- so existing installs never received it.
CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
`dlid_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 of DLID (never store raw DLID)',
`response_data` TEXT NOT NULL COMMENT 'JSON validation response from MokoGitea',
`checked_at` DATETIME NOT NULL,
PRIMARY KEY (`dlid_hash`),
KEY `idx_checked` (`checked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -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 = [])
@@ -85,7 +86,12 @@ class DisplayController extends BaseController
public function sendHeartbeat() public function sendHeartbeat()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => 'Session expired — please reload the page.']);
return;
}
try try
{ {
@@ -800,6 +806,65 @@ class DisplayController extends BaseController
$this->jsonResponse($this->getModel('Import')->importAdminTools()); $this->jsonResponse($this->getModel('Import')->importAdminTools());
} }
// ==================================================================
// Toggle Published
// ==================================================================
public function togglePublished()
{
if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => Text::_('JINVALID_TOKEN')]);
return;
}
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
// ================================================================== // ==================================================================
@@ -0,0 +1,112 @@
<?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'),
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_groups')
. ' WHERE ' . $db->quoteName('condition_id') . ' = ' . $db->quoteName('c.id') . ') AS group_count',
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_rules', 'r')
. ' INNER JOIN ' . $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id')
. ' WHERE ' . $db->quoteName('g.condition_id') . ' = ' . $db->quoteName('c.id') . ') AS rule_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']);
}
$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,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\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);
$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');
}
}
@@ -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,142 @@
<?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';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,152 @@
<?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';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-module badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,142 @@
<?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';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,141 @@
<?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';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,141 @@
<?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';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -120,26 +120,37 @@ document.addEventListener('DOMContentLoaded', function () {
var icon = btn.querySelector('span'); var icon = btn.querySelector('span');
btn.disabled = true; btn.disabled = true;
if (icon) icon.className = 'icon-spinner icon-spin'; if (icon) { icon.className = 'icon-spinner icon-spin'; icon.style.color = ''; }
var fd = new FormData(); var fd = new FormData();
fd.append(token, '1'); fd.append(token, '1');
fetch(url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}}) fetch(url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(function (r) { return r.json(); }) .then(function (r) {
return r.text().then(function (text) {
try { return JSON.parse(text); }
catch (e) { return {success: false, message: 'Server error: ' + text.substring(0, 200)}; }
});
})
.then(function (d) { .then(function (d) {
var msg = d.message || (d.success ? 'Heartbeat sent to HQ.' : 'Heartbeat failed.');
if (d.success) { if (d.success) {
Joomla.renderMessages({message: [d.message || 'Heartbeat sent to HQ.']}); if (icon) { icon.className = 'icon-check'; icon.style.color = '#198754'; }
Joomla.renderMessages({message: [msg]});
} else { } else {
Joomla.renderMessages({error: [d.message || 'Heartbeat failed.']}); if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: [msg]});
} }
}) })
.catch(function () { .catch(function (err) {
Joomla.renderMessages({error: ['Network error sending heartbeat.']}); if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: ['Heartbeat failed: ' + (err.message || 'network error')]});
}) })
.finally(function () { .finally(function () {
btn.disabled = false; btn.disabled = false;
if (icon) icon.className = 'icon-upload'; setTimeout(function () {
if (icon) { icon.className = 'icon-upload'; icon.style.color = ''; }
}, 3000);
}); });
}); });
} }
@@ -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.52</version> <version>02.52.13</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>
@@ -47,6 +47,12 @@
<menu link="option=com_mokosuiteclient&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITECLIENT_MENU_WAFLOG</menu> <menu link="option=com_mokosuiteclient&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITECLIENT_MENU_WAFLOG</menu>
<menu link="option=com_mokosuiteclient&amp;view=database" img="class:database">COM_MOKOSUITECLIENT_MENU_DATABASE</menu> <menu link="option=com_mokosuiteclient&amp;view=database" img="class:database">COM_MOKOSUITECLIENT_MENU_DATABASE</menu>
<menu link="option=com_mokosuiteclient&amp;view=cleanup" img="class:trash">COM_MOKOSUITECLIENT_MENU_CLEANUP</menu> <menu link="option=com_mokosuiteclient&amp;view=cleanup" img="class:trash">COM_MOKOSUITECLIENT_MENU_CLEANUP</menu>
<menu link="option=com_mokosuiteclient&amp;view=conditions" img="class:shuffle">COM_MOKOSUITECLIENT_MENU_CONDITIONS</menu>
<menu link="option=com_mokosuiteclient&amp;view=snippets" img="class:code">COM_MOKOSUITECLIENT_MENU_SNIPPETS</menu>
<menu link="option=com_mokosuiteclient&amp;view=templates" img="class:file-alt">COM_MOKOSUITECLIENT_MENU_TEMPLATES</menu>
<menu link="option=com_mokosuiteclient&amp;view=replacements" img="class:exchange-alt">COM_MOKOSUITECLIENT_MENU_REPLACEMENTS</menu>
<menu link="option=com_mokosuiteclient&amp;view=automation" img="class:random">COM_MOKOSUITECLIENT_MENU_AUTOMATION</menu>
<menu link="option=com_mokosuiteclient&amp;view=modules" img="class:th-large">COM_MOKOSUITECLIENT_MENU_MODULES</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokosuiteclient" img="class:power-off">COM_MOKOSUITECLIENT_MENU_PLUGINS</menu> <menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokosuiteclient" img="class:power-off">COM_MOKOSUITECLIENT_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOSUITECLIENT_MENU_UPDATES</menu> <menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOSUITECLIENT_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITECLIENT_MENU_CHECKIN</menu> <menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITECLIENT_MENU_CHECKIN</menu>
@@ -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.52</version> <version>02.52.13</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>
@@ -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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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>
@@ -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.52</version> <version>02.52.13</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>
@@ -28,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'],
@@ -42,9 +43,10 @@ $iconOverrides = [
'com_mokosuiteclient' => 'icon-shield-alt', 'com_mokosuiteclient' => 'icon-shield-alt',
'com_mokosuitehq' => 'icon-tachometer-alt', 'com_mokosuitehq' => 'icon-tachometer-alt',
'com_mokosuitebackup' => 'icon-archive', 'com_mokosuitebackup' => 'icon-archive',
'com_mokosuitecrm' => 'icon-address-book', 'com_mokosuite_crm' => 'icon-address-book',
'com_mokosuiteerp' => 'icon-briefcase', 'com_mokosuite_erp' => 'icon-briefcase',
'com_mokosuiteshop' => 'icon-shopping-cart', 'com_mokosuiteshop' => 'icon-shopping-cart',
'com_mokoshop' => 'icon-shopping-cart',
'com_mokosuitepos' => 'icon-calculator', 'com_mokosuitepos' => 'icon-calculator',
'com_mokosuitemrp' => 'icon-cogs', 'com_mokosuitemrp' => 'icon-cogs',
'com_mokosuitehrm' => 'icon-id-badge', 'com_mokosuitehrm' => 'icon-id-badge',
@@ -56,8 +58,24 @@ $iconOverrides = [
'com_mokosuiteforms' => 'icon-list-alt', 'com_mokosuiteforms' => 'icon-list-alt',
'com_mokosuitecommunity' => 'icon-comments', 'com_mokosuitecommunity' => 'icon-comments',
'com_mokosuitecross' => 'icon-share-alt', 'com_mokosuitecross' => 'icon-share-alt',
'com_mokoog' => 'icon-globe',
'com_mokosuiteopengraph' => 'icon-globe', 'com_mokosuiteopengraph' => 'icon-globe',
'com_mokosuitestorelocator' => 'icon-map-marker-alt', 'com_mokosuitestorelocator' => 'icon-map-marker-alt',
'com_mokosuiteanalytics' => 'icon-chart-line',
'com_mokosuitesecurity' => 'icon-lock',
'com_mokosuitenotify' => 'icon-bell',
'com_mokosuiteworkflow' => 'icon-random',
'com_mokosuiteai' => 'icon-magic',
'com_mokosuiteauto' => 'icon-car',
'com_mokosuitebeauty' => 'icon-spa',
'com_mokosuiteconstruction' => 'icon-hard-hat',
'com_mokosuiteeditor' => 'icon-edit',
'com_mokosuiteevent' => 'icon-calendar',
'com_mokosuiteinsight' => 'icon-lightbulb',
'com_mokosuitelibrary' => 'icon-book',
'com_mokosuiterealty' => 'icon-home',
'com_mokosuitesupport' => 'icon-life-ring',
'com_mokosuitetaxi' => 'icon-taxi',
]; ];
$childIconMap = [ $childIconMap = [
@@ -262,7 +280,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
$hasChildren = !empty($comp['children']); $hasChildren = !empty($comp['children']);
?> ?>
<?php if ($hasChildren): ?> <?php if ($hasChildren): ?>
<li class="item parent item-level-2 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>"> <li class="item parent item-level-1 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#"> <a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#">
<span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></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> <span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -282,7 +300,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
: ($currentView === $childView); : ($currentView === $childView);
} }
?> ?>
<li class="item mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>"> <li class="item item-level-2 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"' : ''; ?>> <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="<?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> <span class="sidebar-item-title"><?php echo htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -292,7 +310,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
</ul> </ul>
</li> </li>
<?php else: ?> <?php else: ?>
<li class="item mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>"> <li class="item item-level-1 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"' : ''; ?>> <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="<?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> <span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -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.52 * VERSION: 02.52.13
* 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
*/ */
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* VERSION: 02.48.52 * VERSION: 02.52.13
* 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.52 * VERSION: 02.52.13
* 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.52</version> <version>02.52.13</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.52 * VERSION: 02.52.13
* 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.52 * VERSION: 02.52.13
* 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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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>
@@ -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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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.52</version> <version>02.52.13</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.52 * VERSION: 02.52.13
* 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.52</version> <version>02.52.13</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.52 * VERSION: 02.52.13
* 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.52 * VERSION: 02.52.13
* 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.52</version> <version>02.52.13</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>
+1 -1
View File
@@ -2,7 +2,7 @@
<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.52</version> <version>02.52.13</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>
-146
View File
@@ -818,152 +818,6 @@ class Pkg_MokosuiteclientInstallerScript
} }
} }
/**
* Rewrite all Moko Consulting update server URLs from the old
* raw/branch/main pattern to the new clean /updates.xml pattern.
*
* Old: https://git.mokoconsulting.tech/MokoConsulting/{repo}/raw/branch/main/updates.xml
* New: https://git.mokoconsulting.tech/MokoConsulting/{repo}/updates.xml
*/
private function migrateUpdateServerUrls(): void
{
try
{
$db = Factory::getDbo();
$db->setQuery(
"UPDATE " . $db->quoteName('#__update_sites')
. " SET " . $db->quoteName('location') . " = REPLACE("
. $db->quoteName('location') . ", '/raw/branch/main/updates.xml', '/updates.xml')"
. " WHERE " . $db->quoteName('location') . " LIKE " . $db->quote('%mokoconsulting.tech%/raw/branch/main/updates.xml')
);
$db->execute();
$count = $db->getAffectedRows();
if ($count > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Migrated %d Moko update server URL(s) to new format.', $count),
'message'
);
}
}
catch (\Throwable $e)
{
Log::add('Update server URL migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Remove stale and duplicate MokoSuiteClient update site entries.
*
* Keeps only the package-level update site pointing to the dynamic
* MokoGitea endpoint. Removes plugin-level entries, old static URLs,
* and orphaned #__updates rows tied to deleted update sites.
*
* @return void
*
* @since 02.31.00
*/
private function fixUpdateRecords(): void
{
try
{
$db = Factory::getDbo();
// Link orphaned #__updates records to the installed extension
$db->setQuery(
"UPDATE " . $db->quoteName('#__updates') . " u"
. " JOIN " . $db->quoteName('#__extensions') . " e"
. " ON u.element = e.element AND u.type = e.type"
. " SET u.extension_id = e.extension_id"
. " WHERE u.extension_id = 0"
. " AND u.element LIKE " . $db->quote('%mokosuiteclient%')
);
$db->execute();
}
catch (\Throwable $e)
{
// Non-critical
}
}
private function cleanupStaleUpdateSites(): void
{
try
{
$db = Factory::getDbo();
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/updates.xml';
// Find MokoSuiteClient update sites (exclude MokoSuiteClientHQ and other Moko extensions)
$query = $db->getQuery(true)
->select($db->quoteName(['update_site_id', 'location']))
->from($db->quoteName('#__update_sites'))
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteClient%')
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteClient%') . ')')
->where($db->quoteName('name') . ' NOT LIKE ' . $db->quote('%MokoSuiteClientHQ%'))
->where($db->quoteName('location') . ' NOT LIKE ' . $db->quote('%MokoSuiteClientHQ%'));
$db->setQuery($query);
$sites = $db->loadObjectList();
$keepId = null;
$removeIds = [];
foreach ($sites as $site)
{
if ($site->location === $dynamicUrl && $keepId === null)
{
$keepId = (int) $site->update_site_id;
}
else
{
$removeIds[] = (int) $site->update_site_id;
}
}
if (empty($removeIds))
{
return;
}
$idList = implode(',', $removeIds);
// Remove orphaned #__updates rows
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__updates'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
// Remove link rows
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
// Remove stale update sites
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
$count = count($removeIds);
if ($count > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Cleaned up %d stale MokoSuiteClient update site(s).', $count),
'message'
);
}
}
catch (\Throwable $e)
{
Log::add('Error cleaning up stale update sites: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/** /**
* Backup all non-empty extra_query values from update sites. * Backup all non-empty extra_query values from update sites.
+27
View File
@@ -0,0 +1,27 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 02.52.13
-->
<updates>
<update>
<name>MokoSuiteClient</name>
<description>MokoSuiteClient stable build.</description>
<element>pkg_mokosuiteclient</element>
<type>package</type>
<client>site</client>
<version>02.52.13</version>
<creationDate>2026-06-29</creationDate>
<infourl title='MokoSuiteClient'>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/releases/download/stable/pkg_mokosuiteclient-02.52.13.zip</downloadurl>
</downloads>
<sha256>d8016f649e0c011d02be7c6ba141fbe751b94e788330ae27ca797ed1ea5c1213</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update>
</updates>