Compare commits

..

144 Commits

Author SHA1 Message Date
gitea-actions[bot] 8f3d3cea8b chore(version): pre-release bump to 02.47.77-dev [skip ci] 2026-06-23 19:32:02 +00:00
gitea-actions[bot] 44664426f5 chore(version): auto-bump patch 02.47.76-dev [skip ci] 2026-06-23 19:31:37 +00:00
Jonathan Miller 37721cd061 feat: full extension catalog + channel gating by domain
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Generic: Project CI / Lint & Validate (pull_request) Failing after 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 35s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m11s
- catalog.xml: all 21 MokoSuite repos listed with proper categories
  (Platform, Business, Industry, Content, SEO, Modules, Templates)
- Update server URLs point to main branch (stable releases)
- Dev channel only available on *.mokoconsulting.tech domains
- Non-Moko domains forced to stable regardless of Joomla config
- ExtensionsModel respects Joomla's com_installer update_channel
  param on Moko domains, skips dev-tagged versions on stable channel
2026-06-23 14:31:07 -05:00
gitea-actions[bot] ef6052006e chore(version): pre-release bump to 02.47.75-dev [skip ci] 2026-06-23 19:27:44 +00:00
gitea-actions[bot] 19b3b33d70 chore(version): auto-bump patch 02.47.74-dev [skip ci] 2026-06-23 19:27:16 +00:00
Jonathan Miller 0a374ac8d5 feat: fuzzy detection of all MokoSuite/MokoJoom ecosystem packages
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
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 9s
Generic: Project CI / Lint & Validate (pull_request) Failing after 17s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
- DashboardModel.getMokoExtensions() now fuzzy-matches mokosuite*,
  mokosuiteclient*, mokosuitehq*, mokosuitecrm*, mokojoom* across
  packages, components, modules, plugins, and libraries
- Each extension tagged with product family for grouping
- Heartbeat payload includes moko_packages map (element → version)
  so HQ can see all installed MokoSuite products per site
- Menu module already catches com_moko% (no change needed)
2026-06-23 14:26:52 -05:00
gitea-actions[bot] 32e76ecc75 chore(version): pre-release bump to 02.47.73-dev [skip ci] 2026-06-23 19:25:32 +00:00
gitea-actions[bot] d89d0f95f6 chore(version): auto-bump patch 02.47.72-dev [skip ci] 2026-06-23 19:25:01 +00:00
Jonathan Miller 3c4fe24056 feat: enforce ACLs in dashboard WAF sections and sidebar menu
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
Generic: Project CI / Lint & Validate (pull_request) Failing after 57s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 59s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m9s
- Dashboard: WAF chart and Recent WAF Blocks gated behind
  mokosuiteclient.security.waflog ACL
- Menu: all items ACL-gated, added Snippets/Templates/Replacements/
  Conditions entries, removed ticket items, super admins bypass all
2026-06-23 14:24:36 -05:00
gitea-actions[bot] abfdbbcaa2 chore(version): pre-release bump to 02.47.71-dev [skip ci] 2026-06-23 19:23:30 +00:00
gitea-actions[bot] 6e03ff7560 chore(version): auto-bump patch 02.47.70-dev [skip ci] 2026-06-23 19:22:56 +00:00
Jonathan Miller 8cc8cadda2 chore: remove all ticket/helpdesk code — migrated to MokoSuiteCRM
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Generic: Project CI / Lint & Validate (pull_request) Failing after 49s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
- Controller: removed 20+ ticket methods (createTicket, addReply,
  updateStatus, saveCategory, saveCanned, uploadAttachment, etc.)
- Controller: updated VIEW_ACL map — removed tickets/canned/categories,
  added snippets/templates/replacements/conditions
- Controller: WAF log ACL changed from core.admin to security.waflog
- Deleted: TicketsModel, AttachmentService, AutomationEngine,
  NotificationService, TicketsController (API)
- Deleted: all Ticket/Tickets/Ticketsettings/Canned views and templates
  (admin and site)
- Removed importAts method
2026-06-23 14:22:34 -05:00
gitea-actions[bot] 10ef685ab4 chore(version): pre-release bump to 02.47.69-dev [skip ci] 2026-06-23 18:35:28 +00:00
gitea-actions[bot] 79eaebf8a1 chore(version): auto-bump patch 02.47.68-dev [skip ci] 2026-06-23 18:35:01 +00:00
Jonathan Miller 50beb170e4 chore: update access.xml, config.xml, language for all new features
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 23s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 37s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
access.xml: removed stale ticket ACLs, added waflog, impersonate,
  snippets, templates, replacements, conditions actions
config.xml: removed helpdesk/IMAP sections (migrated to CRM), added
  content tools, impersonation settings, updated branding to MokoSuite
language: added all new ACL keys, removed ticket references
2026-06-23 13:28:27 -05:00
gitea-actions[bot] 9418e56dfe chore(version): pre-release bump to 02.47.67-dev [skip ci] 2026-06-23 18:19:04 +00:00
gitea-actions[bot] 157a8a9453 chore(version): pre-release bump to 02.47.66-dev [skip ci] 2026-06-23 18:08:32 +00:00
gitea-actions[bot] 3277ca18c9 chore(version): pre-release bump to 02.47.65-dev [skip ci] 2026-06-23 18:05:58 +00:00
jmiller 4c815e7e81 chore: remove security-audit.yml -- handled by MokoGitea
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 33s
Generic: Project CI / Lint & Validate (pull_request) Successful in 9s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 39s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 30s
2026-06-23 18:05:14 +00:00
Jonathan Miller 60a541fec1 feat: Articles Field, Content Templater — complete Regular Labs suite
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 24s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 59s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 51s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 55s
- Articles Field (#162): custom ListField type for article selection
  in any Joomla form, shows "Title [Category]" options
- Content Templater (#165): {template alias="x"} tags load pre-defined
  article templates from DB, returns introtext+fulltext from JSON
- content_templates table added to install.mysql.sql
- All Regular Labs replacements now implemented:
  #160 conditions + AMM, #161 articles anywhere, #162 articles field,
  #164 conditional content, #165 content templater, #167 email protector,
  #175 rereplacer, #176 snippets, #177 sourcerer, #180 users anywhere,
  #181 cache cleaner
2026-06-23 13:01:32 -05:00
jmiller 2702aea14a chore: remove deploy-manual.yml -- no longer needed
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m13s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m0s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
2026-06-23 17:59:44 +00:00
gitea-actions[bot] 32cd96c92b chore(version): pre-release bump to 02.47.64-dev [skip ci] 2026-06-23 17:57:48 +00:00
gitea-actions[bot] da7f4578d2 chore(version): auto-bump patch 02.47.63-dev [skip ci] 2026-06-23 17:56:56 +00:00
Jonathan Miller db9c68dc5f feat: Articles Anywhere, Sourcerer, Users Anywhere tag engines
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 18s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 53s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
- Articles Anywhere (#161): {article id="42"}[title][introtext]{/article}
  with 18 data tags, date formatting, introtext truncation
- Sourcerer (#177): {source}<?php ... ?>{/source} with forbidden
  function blocklist, PHP execution via eval with output buffering
- Users Anywhere (#180): {user name} for current user, {user id="42"}
  [name][email]{/user} for specific users, email/username masking
- All controlled by params (off by default)
2026-06-23 12:56:39 -05:00
gitea-actions[bot] e513c757b9 chore(version): pre-release bump to 02.47.62-dev [skip ci] 2026-06-23 17:54:05 +00:00
gitea-actions[bot] ce15178dfd chore(version): auto-bump patch 02.47.61-dev [skip ci] 2026-06-23 17:53:41 +00:00
Jonathan Miller 377076e60f style(dashboard): show toggle switch for all configured plugins
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Generic: Project CI / Lint & Validate (pull_request) Successful in 55s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m0s
Toggle switch shown for any plugin with extension_id (installed).
Protected plugins still show badge only. Unconfigured plugins
show no switch or configure button. Removes configure_only logic.
2026-06-23 12:53:20 -05:00
gitea-actions[bot] ed399998d4 chore(version): pre-release bump to 02.47.60-dev [skip ci] 2026-06-23 17:46:39 +00:00
gitea-actions[bot] 50356f8b05 chore(version): auto-bump patch 02.47.59-dev [skip ci] 2026-06-23 17:46:26 +00:00
Jonathan Miller 65ffa835d9 feat: email protector + snippets engine
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
- Email Protector (#167): base64 obfuscation with JS decloaking,
  protects mailto links and plain emails, inject script before </body>
- Snippets (#176): {snippet alias="x" var="val"} tags with DB table,
  variable substitution, nested snippet support (max depth 5)
- Both controlled by params (off by default)
- Snippets table added to install.mysql.sql
2026-06-23 12:46:03 -05:00
gitea-actions[bot] a91d78beff chore(version): pre-release bump to 02.47.58-dev [skip ci] 2026-06-23 17:38:27 +00:00
gitea-actions[bot] 561ea3691a chore(version): auto-bump patch 02.47.57-dev [skip ci] 2026-06-23 17:37:51 +00:00
Jonathan Miller 1fe7c77fbf feat(conditional-content): {show}/{hide} tag processing with conditions engine
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 47s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 59s
- processConditionalTags() handles {show}, {hide}, {else} blocks
- Supports condition="alias_or_id" for saved condition sets
- Supports inline rules: access_level, user_group, menu_item, home_page, date, day, url
- Multiple inline rules per tag use AND logic
- Nested tag support (processes innermost first, up to 10 iterations)
- Runs in onContentPrepare (articles) and onAfterRender (final HTML)
- ConditionsHelper: added passByAlias(), resolveAlias(), evaluateInlineRule()
- Implements #164
2026-06-23 12:37:31 -05:00
gitea-actions[bot] 3c94ffeff3 chore(version): pre-release bump to 02.47.56-dev [skip ci] 2026-06-23 17:34:38 +00:00
gitea-actions[bot] 9067dc62f7 chore(version): auto-bump patch 02.47.55-dev [skip ci] 2026-06-23 17:34:11 +00:00
Jonathan Miller 36bfe59115 feat(amm): module filtering via onPrepareModuleList + conditions engine
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 15s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 25s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m0s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m5s
Frontend modules filtered based on condition sets mapped via
ConditionsHelper::shouldDisplay('com_modules', moduleId).
Modules with no conditions pass through. Graceful skip if tables
don't exist. Implements #160.
2026-06-23 12:33:30 -05:00
gitea-actions[bot] 5b1fe5f806 chore(version): pre-release bump to 02.47.54-dev [skip ci] 2026-06-23 17:32:36 +00:00
gitea-actions[bot] 5855d03ae1 chore(version): auto-bump patch 02.47.53-dev [skip ci] 2026-06-23 17:32:22 +00:00
Jonathan Miller 6090682afd feat(cache): auto-clear Joomla cache on content/extension save
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 35s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 37s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 44s
- autoClearCache() called from onContentAfterSave and onExtensionAfterSave
- Controlled by auto_clear_cache param (default: off)
- Clears all cache groups via Cache::getInstance()->clean('')
- Silent failure — never breaks save operations
2026-06-23 12:32:01 -05:00
Jonathan Miller ae6719049d feat(conditions): add conditions engine DB schema and evaluation API
- Four tables: conditions, conditions_groups, conditions_rules, conditions_map
- ConditionsHelper with pass(), load(), shouldDisplay(), getConditionsForItem()
- 7 rule types: menu_item, home_page, user_group, access_level, date, day, url
- Hierarchical evaluation: condition → groups (AND/OR) → rules (AND/OR)
- Runtime cache for repeated evaluations within same request
- Foundation for Advanced Module Manager (#160) and Conditional Content (#164)
2026-06-23 12:32:00 -05:00
gitea-actions[bot] 98694e46d6 chore(version): pre-release bump to 02.47.52-dev [skip ci] 2026-06-23 17:30:39 +00:00
gitea-actions[bot] 0a6a4d581c chore(version): auto-bump patch 02.47.51-dev [skip ci] 2026-06-23 17:30:26 +00:00
Jonathan Miller d7efb61207 fix(cpanel): add missing sitename/db_type to helper, pill button group
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 17s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Generic: Project CI / Lint & Validate (pull_request) Successful in 41s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
- CpanelHelper now returns sitename and db_type for info bar
- Cache module renders as single pill with three joined buttons
  (domain key, cache clear, temp clear)
2026-06-23 12:29:53 -05:00
gitea-actions[bot] 1e081139e6 chore(version): pre-release bump to 02.47.50-dev [skip ci] 2026-06-23 17:01:28 +00:00
gitea-actions[bot] 0f81e227fc chore(version): auto-bump patch 02.47.49-dev [skip ci] 2026-06-23 17:01:09 +00:00
Jonathan Miller 57b48520af feat: guided tours framework + DevTools reset tour toggle
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Generic: Project CI / Lint & Validate (pull_request) Successful in 48s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
- registerGuidedTours() in script.php postflight registers Welcome and
  Firewall tours via Joomla's #__guidedtours/#__guidedtour_steps tables
- Tours use com_mokosuiteclient.* UIDs, auto-update on package update
- DevTools: reset_tour_prompts one-shot toggle clears all guided tour
  completion flags from #__user_profiles on save
- Language keys added for tour reset field
2026-06-23 11:59:48 -05:00
gitea-actions[bot] bda9ec1192 chore(version): pre-release bump to 02.47.48-dev [skip ci] 2026-06-23 16:48:38 +00:00
gitea-actions[bot] e9af9dc268 chore(version): auto-bump patch 02.47.47-dev [skip ci] 2026-06-23 16:48:18 +00:00
Jonathan Miller d595f23310 fix(cache-module): use Atum header-item structure for status bar items
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 58s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 57s
Each item (domain, cache, temp) wrapped in proper header-item markup
with header-item-icon and header-item-text to match Joomla Atum CSS.
2026-06-23 11:47:46 -05:00
gitea-actions[bot] 0b95419eb6 chore(version): pre-release bump to 02.47.46-dev [skip ci] 2026-06-23 16:47:38 +00:00
gitea-actions[bot] 8a89bc1296 chore(version): auto-bump patch 02.47.45-dev [skip ci] 2026-06-23 16:47:25 +00:00
Jonathan Miller f659c73ffa fix(cache-module): fix AJAX URLs and simplify status bar markup
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 39s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 37s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 36s
Task URLs corrected to display.clearCache/display.clearTemp format.
Removed custom CSS in favor of inline Bootstrap utilities.
2026-06-23 11:47:08 -05:00
gitea-actions[bot] 8e3cd85e3d chore(version): pre-release bump to 02.47.44-dev [skip ci] 2026-06-23 16:46:35 +00:00
gitea-actions[bot] 865b769a71 chore(version): auto-bump patch 02.47.43-dev [skip ci] 2026-06-23 16:46:15 +00:00
Jonathan Miller 10c2c4bbc7 style(cpanel): remove dashboard button, shield links based on ACL
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 46s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 55s
Shield icon links to dashboard only if user has core.manage permission
on com_mokosuiteclient. Otherwise renders as static icon.
2026-06-23 11:45:50 -05:00
gitea-actions[bot] b8cd65c45c chore(version): pre-release bump to 02.47.42-dev [skip ci] 2026-06-23 16:44:41 +00:00
gitea-actions[bot] f86d598610 chore(version): auto-bump patch 02.47.41-dev [skip ci] 2026-06-23 16:44:21 +00:00
Jonathan Miller 06c618dd50 style(cpanel): simplify to single info line with clickable shield
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 18s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 58s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m5s
Single row: shield link, site name, badges, IP, dashboard button.
Removed collapsible body, plugin list, health stats, cache button.
2026-06-23 11:43:53 -05:00
gitea-actions[bot] 56fc3dc065 chore(version): auto-bump patch 02.47.40-dev [skip ci] 2026-06-23 16:41:26 +00:00
Jonathan Miller 8fa87ef1d7 style: label all info bar badges with title + version in both views
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
MokoSuite 02.47.xx | Joomla 6.1.1 | PHP 8.4.20 | mysql
2026-06-23 11:41:02 -05:00
gitea-actions[bot] f1cee7268d chore(version): pre-release bump to 02.47.39-dev [skip ci] 2026-06-23 16:28:59 +00:00
gitea-actions[bot] 7c9c81b2a4 chore(version): auto-bump patch 02.47.38-dev [skip ci] 2026-06-23 16:28:40 +00:00
Jonathan Miller c79a76c9d7 style: unify dashboard and cpanel info bar to same inline badge layout
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 53s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 59s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
Both now use identical structure: shield icon, site name, version badge,
PIN badge, Joomla/PHP/DB badges, debug/offline flags, IP with globe.
Dashboard button restored on cpanel toggle row.
2026-06-23 11:28:27 -05:00
gitea-actions[bot] 08d6140f2a chore(version): pre-release bump to 02.47.37-dev [skip ci] 2026-06-23 16:25:54 +00:00
gitea-actions[bot] 69127e5749 chore(version): pre-release bump to 02.47.36-dev [skip ci] 2026-06-23 16:25:15 +00:00
gitea-actions[bot] f0cf2122f4 chore(version): auto-bump patch 02.47.35-dev [skip ci] 2026-06-23 16:24:49 +00:00
Jonathan Miller d8712c1247 style: add shield icon to dashboard info bar and cpanel module header
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 55s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 57s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 39s
2026-06-23 11:24:35 -05:00
gitea-actions[bot] f4b1059f95 chore(version): pre-release bump to 02.47.34-dev [skip ci] 2026-06-23 16:21:03 +00:00
gitea-actions[bot] 1d3ea606c5 chore(version): auto-bump patch 02.47.33-dev [skip ci] 2026-06-23 16:20:43 +00:00
Jonathan Miller 9d3ec28504 style(cpanel): mirror dashboard info strip in cpanel module header
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 17s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 33s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m0s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m3s
Shows site name, MokoSuite version, Support PIN, Joomla/PHP/DB badges,
debug/offline flags, IP, and dashboard link in a single info row.
Collapse toggle moved below with plugin count summary.
2026-06-23 11:20:26 -05:00
gitea-actions[bot] 1c7452f360 chore(version): pre-release bump to 02.47.32-dev [skip ci] 2026-06-23 16:16:16 +00:00
gitea-actions[bot] 46cfd53052 chore(version): auto-bump patch 02.47.31-dev [skip ci] 2026-06-23 16:15:52 +00:00
Jonathan Miller 0456f467c7 feat(dashboard): add heartbeat send button next to Support PIN
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Project CI / Lint & Validate (pull_request) Failing after 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 53s
Upload icon button beside the PIN badge triggers sendHeartbeat AJAX
call to HQ, which includes the health token (HQ derives the PIN).
2026-06-23 11:15:34 -05:00
gitea-actions[bot] aa9f18525e chore(version): pre-release bump to 02.47.30-dev [skip ci] 2026-06-23 16:12:31 +00:00
gitea-actions[bot] 4ccb916895 chore(version): auto-bump patch 02.47.29-dev [skip ci] 2026-06-23 16:12:03 +00:00
Jonathan Miller fe74ea89a5 style(dashboard): remove ext version bar, 3-column plugin cards
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 4s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m6s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m7s
- Removed extension version bar (versions already shown on each card)
- Plugin cards now 3 per row (col-lg-4) except core category (full width)
2026-06-23 11:11:33 -05:00
gitea-actions[bot] da78796cc1 chore(version): pre-release bump to 02.47.28-dev [skip ci] 2026-06-23 15:55:50 +00:00
gitea-actions[bot] 247c560510 chore(version): auto-bump patch 02.47.27-dev [skip ci] 2026-06-23 15:55:31 +00:00
Jonathan Miller 3390c76719 fix(devtools): use com_content component param for hit suppression
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 49s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 45s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m2s
record_hits is a com_content parameter checked via
ComponentHelper::getParams(), not a global config key.
2026-06-23 10:55:04 -05:00
gitea-actions[bot] 5d5a7dcdbf chore(version): pre-release bump to 02.47.26-dev [skip ci] 2026-06-23 15:51:17 +00:00
gitea-actions[bot] 76e9624ddf chore(version): auto-bump patch 02.47.25-dev [skip ci] 2026-06-23 15:50:52 +00:00
Jonathan Miller 6f7549fa7a fix: address PR review findings — security, performance, cleanup
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 15s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 52s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
- togglePlugin: restrict scope to mokosuiteclient plugins only (was any plugin)
- DevTools: replace full-table hits reset on every request with config toggle
- CurrentIpField: prefer REMOTE_ADDR over spoofable X-Forwarded-For
- SQL: explicit (int) cast on $days interpolation in chart queries
- Heartbeat: enable SSL peer verification (was disabled)
- script.php: remove orphaned docblock
2026-06-23 10:50:37 -05:00
gitea-actions[bot] 91dd1e1eb5 chore(version): pre-release bump to 02.47.24-dev [skip ci] 2026-06-23 15:48:16 +00:00
gitea-actions[bot] 28a4a8cd1b chore(version): auto-bump patch 02.47.23-dev [skip ci] 2026-06-23 15:47:50 +00:00
Jonathan Miller dcaaf007cf fix(dashboard): translate COM_ACTIONLOGS_DISABLED stored in ip_address column
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 43s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 41s
Generic: Repo Health / Access control (pull_request) Successful in 4s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 55s
Joomla stores the literal string 'COM_ACTIONLOGS_DISABLED' in the
action_logs ip_address column when IP logging is turned off. Show
'IP logging off' instead of the raw language key.
2026-06-23 10:47:06 -05:00
gitea-actions[bot] 39837737c5 chore(version): pre-release bump to 02.47.22-dev [skip ci] 2026-06-23 14:07:38 +00:00
gitea-actions[bot] 2a6f5e2d96 chore(version): auto-bump patch 02.47.21-dev [skip ci] 2026-06-23 14:07:26 +00:00
Jonathan Miller 9118eb1fb5 fix(lang): add COM_ACTIONLOGS_DISABLED fallback translation
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 34s
Defines the Joomla core key in our own language file so it renders
properly even when the core language file is missing or corrupt.
2026-06-23 09:07:05 -05:00
gitea-actions[bot] a295f44c57 chore(version): pre-release bump to 02.47.20-dev [skip ci] 2026-06-23 13:20:31 +00:00
gitea-actions[bot] daaf0a73f6 chore(version): pre-release bump to 02.47.19-dev [skip ci] 2026-06-23 13:20:05 +00:00
gitea-actions[bot] af6165d800 chore(version): auto-bump patch 02.47.18-dev [skip ci] 2026-06-23 13:19:46 +00:00
Jonathan Miller 4c4d98cc9f fix(dashboard): require action logs, enable on install, show alert if disabled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m37s
- Install script enables actionlog/joomla and system/actionlogs plugins
- Dashboard shows alert banner with enable link when action logs disabled
- Reverted conditional hiding of UI (action logs is a hard requirement)
2026-06-23 08:19:27 -05:00
Jonathan Miller cfc70ac712 fix(dashboard): handle disabled action logs gracefully
- Check ComponentHelper::isEnabled('com_actionlogs') before rendering
  login chart, View Logs button, and recent logins table
- Show helpful warning when action logs disabled instead of raw
  COM_ACTIONLOGS_DISABLED language key
2026-06-23 08:19:27 -05:00
gitea-actions[bot] e4558c1642 chore(version): pre-release bump to 02.47.17-dev [skip ci] 2026-06-23 13:05:30 +00:00
gitea-actions[bot] 7e73343b4f chore(version): auto-bump patch 02.47.16-dev [skip ci] 2026-06-23 13:05:19 +00:00
Jonathan Miller 29bf75051f style(dashboard): use Bootstrap grid columns for extension version bar
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m16s
Dynamically calculates col-N from extension count (12/count) so items
spread evenly across the full width as horizontal cells, not stacked.
Removed custom flex CSS in favour of Bootstrap row/g-0.
2026-06-23 08:05:02 -05:00
gitea-actions[bot] cca6ae3326 chore(version): pre-release bump to 02.47.15-dev [skip ci] 2026-06-23 12:50:14 +00:00
gitea-actions[bot] d0fc3bdfb1 chore(version): pre-release bump to 02.47.14-dev [skip ci] 2026-06-23 12:50:01 +00:00
gitea-actions[bot] 50c3b621a3 chore(version): auto-bump patch 02.47.13-dev [skip ci] 2026-06-23 12:49:52 +00:00
Jonathan Miller 0aeaea208f feat(dashboard): show frontend/backend app badges on recent logins, full-width ext bar
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
- Recent logins table now parses action log message JSON to display
  Admin/Site badge indicating where each login occurred
- Login query uses exact match on LOGGED_IN key (excludes logout events)
- Login limit increased from 5 to 10
- Info bar reverted from stacked column to horizontal row layout
- Extension version bar uses flex: 1 1 0 for full-width auto-sized cells
- Updated CHANGELOG
2026-06-23 07:47:43 -05:00
gitea-actions[bot] 95e9e618aa chore(version): pre-release bump to 02.47.12-dev [skip ci] 2026-06-23 03:22:01 +00:00
gitea-actions[bot] 112e2810bd chore(version): pre-release bump to 02.47.11-dev [skip ci] 2026-06-23 00:39:25 +00:00
gitea-actions[bot] 4088815485 chore(version): auto-bump patch 02.47.10-dev [skip ci] 2026-06-23 00:39:12 +00:00
Jonathan Miller 328acb462b style(dashboard): full-width info bar with stacked labels and auto-sizing
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 33s
- Items distribute evenly across full width via justify-content: space-between
- Labels stack above values (flex-direction: column) for cleaner layout
- Smaller label text (0.7rem), consistent badge sizing
- Debug/Offline badges wrapped in info-items for consistent spacing
- MokoSuiteClient → MokoSuite in version label
2026-06-22 19:38:55 -05:00
gitea-actions[bot] ee27f0a3b2 chore(version): pre-release bump to 02.47.09-dev [skip ci] 2026-06-22 14:18:38 +00:00
gitea-actions[bot] e70539f817 chore(version): pre-release bump to 02.47.08-dev [skip ci] 2026-06-22 00:35:51 +00:00
gitea-actions[bot] 4c39c583c0 chore(version): pre-release bump to 02.47.07-dev [skip ci] 2026-06-22 00:21:17 +00:00
gitea-actions[bot] 72da5ca1b5 chore(version): auto-bump patch 02.47.06-dev [skip ci] 2026-06-22 00:21:03 +00:00
Jonathan Miller 97670dbd54 style: display MokoSuite instead of MokoSuiteClient everywhere
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
2026-06-21 19:20:45 -05:00
gitea-actions[bot] 4e1b9b8044 chore(version): pre-release bump to 02.47.05-dev [skip ci] 2026-06-21 23:48:15 +00:00
gitea-actions[bot] 220e52fb71 chore(version): pre-release bump to 02.47.04-dev [skip ci] 2026-06-21 23:45:27 +00:00
gitea-actions[bot] c4735a73e8 chore(version): pre-release bump to 02.47.03-dev [skip ci] 2026-06-21 23:32:26 +00:00
gitea-actions[bot] d430e083e4 chore(version): pre-release bump to 02.47.02-dev [skip ci] 2026-06-21 23:31:58 +00:00
Jonathan Miller 52ad2f2b37 docs: update changelog and README for v02.47 release cycle
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
2026-06-21 18:27:24 -05:00
gitea-actions[bot] 0308df3b53 chore(version): pre-release bump to 02.47.01-dev [skip ci] 2026-06-21 23:26:05 +00:00
Jonathan Miller 21df82dc59 chore: remove ticket guided tour and support menu, rename aliases tab
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 41s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
- Removed helpdesk guided tour (tickets moved to MokoSuiteCRM)
- setupSupportMenuItem returns early (no ticket menu)
- Renamed tab: Mirror Domains & Staging Environments
2026-06-21 18:15:59 -05:00
gitea-actions[bot] 9ad828c248 chore(version): pre-release bump to 02.47.00-dev [skip ci] 2026-06-21 23:13:05 +00:00
gitea-actions[bot] 15e891dca2 chore(version): auto-bump patch 02.46.99-dev [skip ci] 2026-06-21 23:12:45 +00:00
Jonathan Miller 6ee0f08f42 fix: undefined $pluginDir in reinstallBrokenPlugins Step 1
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
2026-06-21 18:04:54 -05:00
gitea-actions[bot] d0ff29aed2 chore(version): pre-release bump to 02.46.98-dev [skip ci] 2026-06-21 23:02:06 +00:00
gitea-actions[bot] 6186eec2ca chore(version): pre-release bump to 02.46.97-dev [skip ci] 2026-06-21 23:01:25 +00:00
gitea-actions[bot] 4566071d74 chore(version): auto-bump patch 02.46.96-dev [skip ci] 2026-06-21 23:01:03 +00:00
Jonathan Miller 4a8570f7a3 feat(devtools): Domain Aliases & Staging — repeatable subform table
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 55s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m8s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 26s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Renamed from "Dev Domain" to "Domain Aliases & Staging". Now a
repeatable table (subform) supporting multiple domain aliases, each
with: domain, offline bypass, robots directive, and label.

Core plugin reads all aliases and matches current host against the
list. Auto-includes dev.{primary_domain} when no aliases configured.
2026-06-21 17:59:21 -05:00
gitea-actions[bot] 756e8b664b chore(version): pre-release bump to 02.46.95-dev [skip ci] 2026-06-21 22:51:49 +00:00
gitea-actions[bot] a8224bce93 chore(version): auto-bump patch 02.46.94-dev [skip ci] 2026-06-21 22:51:13 +00:00
Jonathan Miller 711b89ea03 fix(install): two-step plugin reinstall — extract files then create DB records
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Generic: Project CI / Lint & Validate (pull_request) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 34s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 39s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 35s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Step 1: Extract plugin zips to correct directories (always re-extract)
Step 2: For each expected plugin with files but no DB record, INSERT
with all required fields. Set namespace via separate UPDATE (handles
Joomla versions without namespace column).
2026-06-21 17:51:01 -05:00
gitea-actions[bot] 98d7ab3bb3 chore(version): pre-release bump to 02.46.93-dev [skip ci] 2026-06-21 22:43:47 +00:00
gitea-actions[bot] ee4746e8ff chore(version): pre-release bump to 02.46.92-dev [skip ci] 2026-06-21 22:41:46 +00:00
gitea-actions[bot] 6fa3cbbaea chore(version): auto-bump patch 02.46.91-dev [skip ci] 2026-06-21 22:41:31 +00:00
Jonathan Miller 0e2433cf5c fix(install): use Joomla Installer API for plugin reinstall
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 14s
Universal: Auto Version Bump / Version Bump (push) Successful in 23s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 56s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 58s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Replace raw ZipArchive extract with Joomla\CMS\Installer\Installer::install()
which properly handles namespace registration, extension record creation,
and all internal bookkeeping. Removes manual INSERT from enablePlugin()
since the Installer handles it correctly.
2026-06-21 17:41:09 -05:00
gitea-actions[bot] 59f50867c0 chore(version): pre-release bump to 02.46.90-dev [skip ci] 2026-06-21 22:36:01 +00:00
gitea-actions[bot] 941d49e0ce chore(version): pre-release bump to 02.46.89-dev [skip ci] 2026-06-21 22:33:26 +00:00
gitea-actions[bot] ad7af89228 chore(version): pre-release bump to 02.46.88-dev [skip ci] 2026-06-21 22:30:04 +00:00
gitea-actions[bot] 078caa423b chore(version): auto-bump patch 02.46.87-dev [skip ci] 2026-06-21 22:29:49 +00:00
Jonathan Miller 1583e98cea fix(install): always re-extract plugin zips on update, pretty cpanel labels
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Generic: Project CI / Lint & Validate (pull_request) Successful in 18s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m0s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m5s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- reinstallBrokenPlugins now re-extracts ALL expected plugins on every
  update (removes old dir first) to ensure files stay current
- Cpanel plugin labels: Core Engine, Web Firewall, Tenant Guard,
  Dev Tools, Offline Bypass, GeoIP Lookup, License Manager
2026-06-21 17:26:54 -05:00
gitea-actions[bot] a737ac9106 chore(version): pre-release bump to 02.46.86-dev [skip ci] 2026-06-21 22:22:00 +00:00
gitea-actions[bot] d03269c1ca chore(version): auto-bump patch 02.46.85-dev [skip ci] 2026-06-21 22:21:46 +00:00
Jonathan Miller 29079c10e2 feat(devtools): implement dev domain offline bypass and robots logic
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 42s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 47s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
handleSiteAlias() and injectAliasRobots() now read from DevTools
plugin params instead of hardcoding dev.{primary_domain}. Supports
custom domain, configurable offline bypass, and robots directive.
Uses static cache to avoid repeated DB queries per request.
2026-06-21 17:20:19 -05:00
gitea-actions[bot] b433865a6d chore(version): pre-release bump to 02.46.84-dev [skip ci] 2026-06-21 21:41:00 +00:00
gitea-actions[bot] b3cba3ea78 chore(version): pre-release bump to 02.46.83-dev [skip ci] 2026-06-21 21:26:22 +00:00
gitea-actions[bot] 3e79014b97 chore(version): auto-bump patch 02.46.82-dev [skip ci] 2026-06-21 21:26:08 +00:00
Jonathan Miller 62d213027f Merge remote-tracking branch 'origin/dev' into dev
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 39s
Generic: Project CI / Lint & Validate (pull_request) Successful in 49s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 53s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
# Conflicts:
#	source/packages/plg_task_mokosuiteclient_tickets/mokosuiteclient_tickets.xml
2026-06-21 16:25:48 -05:00
Jonathan Miller b2d1e4ba23 chore: remove helpdesk/ticket system (migrating to MokoSuiteCRM #67)
- Removed ticket views, models, controllers (ticket, tickets, ticketsettings)
- Removed plg_task_mokosuiteclient_tickets plugin and source
- Removed from package manifest, enablePlugin, protectExtensions
- Removed Helpdesk entry from sidebar menu
- Ticket tables kept in SQL for migration compatibility
- See MokoSuiteClient#233, MokoSuiteCRM#67
2026-06-21 15:50:29 -05:00
gitea-actions[bot] d4c4d1be2d chore(version): pre-release bump to 02.46.81-dev [skip ci] 2026-06-21 20:35:25 +00:00
93 changed files with 3232 additions and 6573 deletions
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation # INGROUP: moko-platform.Automation
# VERSION: 02.46.80 # VERSION: 02.47.77
# 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"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+38 -6
View File
@@ -14,21 +14,53 @@
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.46.80 VERSION: 02.47.77
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [02.44.00] --- 2026-06-20 ### Added
- **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
- **Domain as support key** — click-to-copy domain in admin status bar
- **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
- **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
- **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
- 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.44.00] --- 2026-06-20
## [02.43.00] --- 2026-06-20
## [02.43.00] --- 2026-06-20
## [02.42.00] --- 2026-06-20 ## [02.42.00] --- 2026-06-20
## [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.46.80 VERSION: 02.47.77
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.46.80 VERSION: 02.47.77
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.46.80 VERSION: 02.47.77
BRIEF: Project license (GPL-3.0-or-later) BRIEF: Project license (GPL-3.0-or-later)
--> -->
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
+32 -9
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.46.80 VERSION: 02.47.77
PATH: /README.md PATH: /README.md
BRIEF: MokoSuiteClient platform plugin for Joomla BRIEF: MokoSuiteClient platform plugin for Joomla
--> -->
@@ -21,17 +21,40 @@
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) [![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoSuiteClient is a Joomla 5.x / 6.x system plugin package that provides white-label branding, security hardening, tenant restrictions, health monitoring, and multi-domain management for the MokoSuiteClient platform. MokoSuiteClient is the Joomla 5.x / 6.x client-facing tracker and identity layer for the MokoSuite platform. It provides security hardening, health monitoring, privacy compliance, multi-domain management, and integration with MokoSuiteHQ for centralized site management.
## Features ## Features
- **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS ### Core
- **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control - **Admin Dashboard** — site info, plugin status, quick actions, support PIN
- **Health Monitoring** — 16 diagnostic checks via `/?mokosuiteclient=health` with Grafana auto-provisioning - **Health Monitoring** — 15 diagnostic checks via `/?mokosuiteclient=health`
- **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs - **Heartbeat** — RSA-signed registration with MokoSuiteHQ, daily support PIN rotation
- **Remote API** — 6 endpoints (health, install, update, cache, backup, info) - **Extension Catalog** — browse and install Moko Consulting extensions
- **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions
- **Plugin Protection** — protected status, hidden from non-master users, disable/uninstall blocked ### Security (Firewall Plugin)
- **Web Application Firewall** — SQL injection, XSS, RFI, directory traversal shields
- **Security Headers** — X-Frame-Options, CSP, HSTS, Referrer-Policy, Permissions-Policy
- **IP Management** — trusted IPs, blocklist, auto-ban on WAF threshold
- **Password Policy** — min length, uppercase, number, special character requirements
- **Access Control** — admin secret URL, frontend super user block, upload restrictions
### Privacy Guard
- **GDPR Compliance** — data subject requests, consent logging, retention policies
- **User Data** — export, anonymize, or delete user data on request
### DevTools
- **Development Mode** — debug, cache disable, hit suppression
- **Mirror Domains & Staging** — repeatable table of domain aliases with offline bypass and robots directives
- **Maintenance** — reset hits, delete versions, reset download keys
### Multi-Domain
- **Site Aliases** — per-alias offline mode, robots directives, canonical URLs
- **Offline Bypass** — TOS, privacy policy, and support pages remain accessible when site is offline
### Integration
- **MokoSuiteHQ** — heartbeat, health data, backup status, activity logging
- **MokoSuiteBackup** — bridge plugin discovers BackupStatusHelper for heartbeat payloads
- **Joomla** — guided tours, action logging, custom fields, scheduled tasks
## Requirements ## Requirements
+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.46.80 VERSION: 02.47.77
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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Build Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Configuration Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Installation Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Operations Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Testing Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Troubleshooting Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Documentation Index (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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.46.80) # MokoSuiteClient Plugin Overview (VERSION: 02.47.77)
## 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.46.80 VERSION: 02.47.77
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
--> -->
@@ -1,15 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<access component="com_mokosuiteclient"> <access component="com_mokosuiteclient">
<section name="component"> <section name="component">
<!-- Core Joomla ACL -->
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" /> <action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" /> <action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
<!-- Dashboard & UI -->
<action name="mokosuiteclient.dashboard" title="COM_MOKOSUITECLIENT_ACL_DASHBOARD" description="COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC" /> <action name="mokosuiteclient.dashboard" title="COM_MOKOSUITECLIENT_ACL_DASHBOARD" description="COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC" />
<action name="mokosuiteclient.extensions" title="COM_MOKOSUITECLIENT_ACL_EXTENSIONS" description="COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC" />
<action name="mokosuiteclient.htaccess" title="COM_MOKOSUITECLIENT_ACL_HTACCESS" description="COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC" />
<action name="mokosuiteclient.tickets" title="COM_MOKOSUITECLIENT_ACL_TICKETS" description="COM_MOKOSUITECLIENT_ACL_TICKETS_DESC" />
<action name="mokosuiteclient.tickets.create" title="COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE" description="COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE_DESC" />
<action name="mokosuiteclient.tickets.assign" title="COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN" description="COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN_DESC" />
<action name="mokosuiteclient.plugins.toggle" title="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE" description="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC" /> <action name="mokosuiteclient.plugins.toggle" title="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE" description="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC" />
<action name="mokosuiteclient.cache" title="COM_MOKOSUITECLIENT_ACL_CACHE" description="COM_MOKOSUITECLIENT_ACL_CACHE_DESC" /> <action name="mokosuiteclient.cache" title="COM_MOKOSUITECLIENT_ACL_CACHE" description="COM_MOKOSUITECLIENT_ACL_CACHE_DESC" />
<!-- Security -->
<action name="mokosuiteclient.security.waflog" title="COM_MOKOSUITECLIENT_ACL_WAFLOG" description="COM_MOKOSUITECLIENT_ACL_WAFLOG_DESC" />
<action name="mokosuiteclient.security.impersonate" title="COM_MOKOSUITECLIENT_ACL_IMPERSONATE" description="COM_MOKOSUITECLIENT_ACL_IMPERSONATE_DESC" />
<!-- Content Tools -->
<action name="mokosuiteclient.snippets.manage" title="COM_MOKOSUITECLIENT_ACL_SNIPPETS" description="COM_MOKOSUITECLIENT_ACL_SNIPPETS_DESC" />
<action name="mokosuiteclient.templates.manage" title="COM_MOKOSUITECLIENT_ACL_TEMPLATES" description="COM_MOKOSUITECLIENT_ACL_TEMPLATES_DESC" />
<action name="mokosuiteclient.replacements.manage" title="COM_MOKOSUITECLIENT_ACL_REPLACEMENTS" description="COM_MOKOSUITECLIENT_ACL_REPLACEMENTS_DESC" />
<action name="mokosuiteclient.conditions.manage" title="COM_MOKOSUITECLIENT_ACL_CONDITIONS" description="COM_MOKOSUITECLIENT_ACL_CONDITIONS_DESC" />
<!-- Extensions & Catalog -->
<action name="mokosuiteclient.extensions" title="COM_MOKOSUITECLIENT_ACL_EXTENSIONS" description="COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC" />
</section> </section>
</access> </access>
@@ -1,122 +1,219 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
Extension catalog for MokoSuiteClient Extension Manager. MokoSuite Extension Catalog
Each entry points to the extension's own updates.xml. The installer Each entry points to the extension's updates.xml on the main branch.
resolves the latest version and download URL at runtime, respecting The installer resolves the latest version and download URL at runtime,
the site's configured update channel (dev/stable). respecting the site's configured update channel (stable/dev) from
Joomla's com_installer params.
To add an extension: copy an <extension> block and fill in the fields.
--> -->
<catalog> <catalog>
<!-- ═══════════════════════════════════════════════════════════════════
Platform (Layer 0)
═══════════════════════════════════════════════════════════════════ -->
<extension> <extension>
<name>MokoSuiteClient</name> <name>MokoSuiteClient</name>
<element>pkg_mokosuiteclient</element> <element>pkg_mokosuiteclient</element>
<type>package</type> <type>package</type>
<description>Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.</description> <description>Admin dashboard, security firewall, tenant restrictions, health monitoring, content tools, and REST API.</description>
<icon>icon-shield-alt</icon> <icon>icon-shield-alt</icon>
<category>Platform</category> <category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclient-platform</article>
<protected>true</protected> <protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoSuiteClientHQ</name> <name>MokoSuiteHQ</name>
<element>pkg_mokosuiteclienthq</element> <element>pkg_mokosuitehq</element>
<type>package</type> <type>package</type>
<description>Centralized control panel for managing all MokoSuiteClient client installations.</description> <description>Centralized control panel for managing all MokoSuite client installations.</description>
<icon>icon-tachometer-alt</icon> <icon>icon-tachometer-alt</icon>
<category>Platform</category> <category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclient-base</article> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/main/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClientHQ/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoOnyx</name> <name>MokoSuiteBackup</name>
<element>mokoonyx</element> <element>pkg_mokosuitebackup</element>
<type>template</type>
<description>Modern Joomla site template with dark mode, custom layouts, and MokoSuiteClient integration.</description>
<icon>icon-paint-brush</icon>
<category>Templates</category>
<article>https://mokoconsulting.tech/support/products/mokoonyx-template</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomOpenGraph</name>
<element>pkg_mokoog</element>
<type>package</type>
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
<icon>icon-share-alt</icon>
<category>SEO</category>
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteClientBackup</name>
<element>pkg_mokojoombackup</element>
<type>package</type> <type>package</type>
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description> <description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
<icon>icon-archive</icon> <icon>icon-archive</icon>
<category>Tools</category> <category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclientbackup</article> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/raw/branch/main/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClientBackup/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<!-- ═══════════════════════════════════════════════════════════════════
Business Suite (Layers 1-4)
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoSuiteCRM</name>
<element>pkg_mokosuitecrm</element>
<type>package</type>
<description>Layer 1 — Contacts, deals pipeline, activities, e-signature, email integration, helpdesk.</description>
<icon>icon-address-book</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteERP</name>
<element>pkg_mokosuiteerp</element>
<type>package</type>
<description>Layer 2 — Products, orders, invoicing, inventory, warehouses, accounting, payments.</description>
<icon>icon-briefcase</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteERP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteShop</name>
<element>pkg_mokosuiteshop</element>
<type>package</type>
<description>Layer 3 — Product catalog, shopping cart, checkout, coupons. Requires MokoSuiteERP.</description>
<icon>icon-shopping-cart</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteShop/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuitePOS</name>
<element>pkg_mokosuitepos</element>
<type>package</type>
<description>Layer 3 — Touch-screen POS, multi-terminal, cash register, receipt printing.</description>
<icon>icon-calculator</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuitePOS/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteMRP</name>
<element>pkg_mokosuitemrp</element>
<type>package</type>
<description>Layer 3 — BOM, manufacturing orders, workstation management, production scheduling.</description>
<icon>icon-cog</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteMRP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteHRM</name>
<element>pkg_mokosuitehrm</element>
<type>package</type>
<description>Layer 3 — Human Resource Management: employees, leave, expenses, payroll, recruiting.</description>
<icon>icon-users</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteRestaurant</name>
<element>pkg_mokosuiterestaurant</element>
<type>package</type>
<description>Layer 4 — Floor plan, table management, kitchen display, split bills, online ordering.</description>
<icon>icon-utensils</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteRestaurant/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteChild</name>
<element>pkg_mokosuitechild</element>
<type>package</type>
<description>Layer 2 — Child Care Management: enrollment, attendance, billing, parent portal.</description>
<icon>icon-child</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteChild/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteNPO</name>
<element>pkg_mokosuitenpo</element>
<type>package</type>
<description>Nonprofit management: donors, donations, campaigns, grants, volunteers, events.</description>
<icon>icon-heart</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteNPO/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteField</name>
<element>pkg_mokosuitefield</element>
<type>package</type>
<description>Field Service — dispatch, work orders, scheduling, mobile tech, plumbing/HVAC.</description>
<icon>icon-wrench</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCreate</name>
<element>pkg_mokosuitecreate</element>
<type>package</type>
<description>Layer 2 — Creative Agency: projects, tasks, timesheets, client proofing.</description>
<icon>icon-paint-brush</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCreate/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Content & Community
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoSuiteForms</name>
<element>pkg_mokosuiteforms</element>
<type>package</type>
<description>Form builder — custom forms, submissions, notifications, and data exports.</description>
<icon>icon-list-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteForms/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCommunity</name>
<element>pkg_mokosuitecommunity</element>
<type>package</type>
<description>Community profiles, connections, and activity streams for Joomla.</description>
<icon>icon-users</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCommunity/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCross</name>
<element>pkg_mokosuitecross</element>
<type>package</type>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms.</description>
<icon>icon-share-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteOpenGraph</name>
<element>pkg_mokosuiteopengraph</element>
<type>package</type>
<description>Open Graph, Twitter Card, JSON-LD structured data, and social sharing meta tags.</description>
<icon>icon-share-alt</icon>
<category>SEO</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteStoreLocator</name>
<element>pkg_mokosuitestorelocator</element>
<type>package</type>
<description>Interactive map, location search, and admin management for store locations.</description>
<icon>icon-map-marker-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Standalone Extensions (MokoJoom)
═══════════════════════════════════════════════════════════════════ -->
<extension> <extension>
<name>MokoJoomHero</name> <name>MokoJoomHero</name>
<element>mod_mokojoomhero</element> <element>mod_mokojoomhero</element>
<type>module</type> <type>module</type>
<description>Random hero image module from a configurable folder.</description> <description>Hero module — image slideshow, video backgrounds, solid color/gradient, parallax.</description>
<icon>icon-image</icon> <icon>icon-image</icon>
<category>Modules</category> <category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokojoomhero</article> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<!-- ═══════════════════════════════════════════════════════════════════
Templates
═══════════════════════════════════════════════════════════════════ -->
<extension> <extension>
<name>MokoJoomCommunity</name> <name>MokoOnyx</name>
<element>pkg_mokojoomcommunity</element> <element>mokoonyx</element>
<type>package</type> <type>template</type>
<description>Community Builder integration package with custom fields and user management.</description> <description>Modern Joomla site template with dark mode, custom layouts, and MokoSuite integration.</description>
<icon>icon-users</icon> <icon>icon-paint-brush</icon>
<category>Community</category> <category>Templates</category>
<article>https://mokoconsulting.tech/support/products/mokojoomcommunity</article> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCommunity/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomCross</name>
<element>plg_system_mokojoomcross</element>
<type>plugin</type>
<description>Cross-extension integration plugin for Joomla component interoperability.</description>
<icon>icon-link</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokojoomcross</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomStoreLocator</name>
<element>mod_mokojoomstorelocator</element>
<type>module</type>
<description>Store locator module with Google Maps integration and search.</description>
<icon>icon-map-marker-alt</icon>
<category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokojoomstorelocator</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>DPCalendar API</name>
<element>mokodpcalendarapi</element>
<type>plugin</type>
<description>Web Services plugin exposing DPCalendar events and calendars via REST API.</description>
<icon>icon-calendar</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokodpcalendarapi</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>Gallery Calendar</name>
<element>mokogallerycalendar</element>
<type>plugin</type>
<description>JoomGallery and DPCalendar integration — link galleries to events.</description>
<icon>icon-images</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
</catalog> </catalog>
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<config> <config>
<fieldset name="general" label="General" description="General component settings."> <fieldset name="general" label="General" description="General component settings.">
<field name="brand_name" type="text" default="MokoSuiteClient" <field name="brand_name" type="text" default="MokoSuite"
label="Brand Name" label="Brand Name"
description="Displayed in the admin sidebar, dashboard, and emails." description="Displayed in the admin sidebar, dashboard, and emails."
hint="MokoSuiteClient" /> hint="MokoSuite" />
<field name="support_email" type="email" default="" <field name="support_email" type="email" default=""
label="Support Email" label="Support Email"
description="Reply-to address for outbound notification emails." description="Reply-to address for outbound notification emails."
hint="support@example.com" /> hint="support@example.com" />
</fieldset> </fieldset>
<fieldset name="notifications" label="Email Notifications" description="Configure email recipients for ticket and security notifications."> <fieldset name="notifications" label="Notifications" description="Email and push notification settings.">
<field name="admin_emails" type="text" default="" <field name="admin_emails" type="text" default=""
label="Admin Email Addresses" label="Admin Email Addresses"
description="Comma-separated email addresses to receive all notifications." description="Comma-separated email addresses to receive all notifications."
@@ -31,7 +31,7 @@
<field name="spacer_ntfy" type="spacer" label="Push Notifications (ntfy)" /> <field name="spacer_ntfy" type="spacer" label="Push Notifications (ntfy)" />
<field name="ntfy_enabled" type="radio" default="0" <field name="ntfy_enabled" type="radio" default="0"
label="Enable ntfy Push" label="Enable ntfy Push"
description="Send push notifications via ntfy for ticket and security events." description="Send push notifications via ntfy for security and system events."
class="btn-group btn-group-yesno"> class="btn-group btn-group-yesno">
<option value="1">JYES</option> <option value="1">JYES</option>
<option value="0">JNO</option> <option value="0">JNO</option>
@@ -40,13 +40,13 @@
label="ntfy Server URL" label="ntfy Server URL"
description="Full URL to your ntfy server." description="Full URL to your ntfy server."
showon="ntfy_enabled:1" /> showon="ntfy_enabled:1" />
<field name="ntfy_topic" type="text" default="mokosuiteclient-tickets" <field name="ntfy_topic" type="text" default="mokosuite-alerts"
label="Ticket Topic" label="Alert Topic"
description="ntfy topic name for helpdesk ticket notifications." description="ntfy topic name for general alert notifications."
showon="ntfy_enabled:1" /> showon="ntfy_enabled:1" />
<field name="ntfy_security_topic" type="text" default="mokosuiteclient-security" <field name="ntfy_security_topic" type="text" default="mokosuite-security"
label="Security Topic" label="Security Topic"
description="ntfy topic name for security alert notifications. Falls back to ticket topic if empty." description="ntfy topic name for security alerts. Falls back to alert topic if empty."
showon="ntfy_enabled:1" /> showon="ntfy_enabled:1" />
<field name="ntfy_token" type="password" default="" <field name="ntfy_token" type="password" default=""
label="ntfy Auth Token" label="ntfy Auth Token"
@@ -54,59 +54,42 @@
showon="ntfy_enabled:1" /> showon="ntfy_enabled:1" />
</fieldset> </fieldset>
<fieldset name="helpdesk" label="Helpdesk Settings" description="Default helpdesk behavior."> <fieldset name="content_tools" label="Content Tools" description="Settings for content tag engines and replacements.">
<field name="default_category" type="sql" default="" <field name="spacer_snippets" type="spacer" label="Snippets" />
label="Default Ticket Category" <field name="snippets_default_category" type="text" default=""
description="Category assigned to tickets without a selection." label="Default Snippet Category"
query="SELECT id AS value, title AS text FROM #__mokosuiteclient_ticket_categories WHERE published = 1 ORDER BY ordering" /> description="Category assigned to new snippets if none selected." />
<field name="autoclose_days" type="number" default="7"
label="Auto-Close After (days)" <field name="spacer_templates" type="spacer" label="Content Templates" />
description="Resolved tickets are auto-closed after this many days. 0 = disabled." /> <field name="templates_default_category" type="text" default=""
<field name="kb_search_enabled" type="radio" default="1" label="Default Template Category"
label="KB Search on Ticket Forms" description="Category assigned to new content templates if none selected." />
description="Show knowledge base search before ticket submission."
class="btn-group btn-group-yesno"> <field name="spacer_replacements" type="spacer" label="Replacements" />
<option value="1">JYES</option> <field name="replacements_max_rules" type="number" default="100"
<option value="0">JNO</option> label="Max Active Rules"
</field> description="Maximum number of replacement rules processed per page load. 0 = unlimited." />
<field name="satisfaction_enabled" type="radio" default="1"
label="Satisfaction Ratings"
description="Show rating prompt on resolved tickets."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="max_attachment_size" type="number" default="10"
label="Max Attachment Size (MB)"
description="Maximum upload size per file in megabytes." />
</fieldset> </fieldset>
<fieldset name="email_to_ticket" label="Email-to-Ticket (IMAP)" description="Create tickets from incoming emails via IMAP polling."> <fieldset name="impersonation" label="User Impersonation" description="Skeleton Key — log into the frontend as another user for support.">
<field name="imap_host" type="text" default="" <field name="skeleton_key_control_groups" type="usergrouplist" default="8"
label="IMAP Server" label="Groups Allowed to Impersonate"
description="IMAP hostname (e.g. imap.gmail.com)" description="User groups that can log in as another user."
hint="imap.gmail.com" /> multiple="true"
<field name="imap_port" type="number" default="993" layout="joomla.form.field.list-fancy-select" />
label="Port" <field name="skeleton_key_target_groups" type="usergrouplist" default="2"
description="IMAP port (993 for SSL, 143 for plain)" /> label="Groups That Can Be Impersonated"
<field name="imap_ssl" type="radio" default="1" description="User groups whose accounts can be accessed via impersonation."
label="Use SSL" multiple="true"
class="btn-group btn-group-yesno"> layout="joomla.form.field.list-fancy-select" />
<option value="1">JYES</option> <field name="skeleton_key_blocked_groups" type="usergrouplist" default="7,8"
<option value="0">JNO</option> label="Groups That Cannot Be Impersonated"
</field> description="User groups protected from impersonation (overrides target groups)."
<field name="imap_user" type="text" default="" multiple="true"
label="Username" layout="joomla.form.field.list-fancy-select" />
description="IMAP login username or email address." /> <field name="skeleton_key_cookie_lifetime" type="number" default="10"
<field name="imap_password" type="password" default="" label="Cookie Lifetime (seconds)"
label="Password" description="How long the impersonation cookie remains valid. Short values are more secure." />
description="IMAP password or app-specific password." />
<field name="imap_folder" type="text" default="INBOX"
label="Inbox Folder"
description="IMAP folder to poll for new messages." />
<field name="imap_processed_folder" type="text" default="INBOX.Processed"
label="Processed Folder"
description="Move processed emails to this folder. Leave empty to just mark as read." />
</fieldset> </fieldset>
<fieldset name="permissions" label="COM_MOKOSUITECLIENT_ACL_TITLE" <fieldset name="permissions" label="COM_MOKOSUITECLIENT_ACL_TITLE"
@@ -1,8 +1,11 @@
; MokoSuiteClient Admin Dashboard - Language Strings ; MokoSuite Admin Dashboard - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved. ; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuiteClient Control Panel" COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuite Control Panel"
; Joomla core fallback keys
COM_ACTIONLOGS_DISABLED="User Action Logging is disabled. Please enable the &quot;Action Log - Joomla&quot; plugin."
COM_MOKOSUITECLIENT_SITE="Site" COM_MOKOSUITECLIENT_SITE="Site"
COM_MOKOSUITECLIENT_DATABASE="Database" COM_MOKOSUITECLIENT_DATABASE="Database"
COM_MOKOSUITECLIENT_DEBUG_ON="Debug ON" COM_MOKOSUITECLIENT_DEBUG_ON="Debug ON"
@@ -20,22 +23,27 @@ COM_MOKOSUITECLIENT_EXTENSIONS_TITLE="Moko Extensions"
COM_MOKOSUITECLIENT_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server." COM_MOKOSUITECLIENT_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server."
COM_MOKOSUITECLIENT_EXTENSIONS_LINK="Moko Extensions" COM_MOKOSUITECLIENT_EXTENSIONS_LINK="Moko Extensions"
COM_MOKOSUITECLIENT_HTACCESS_TITLE=".htaccess Maker" COM_MOKOSUITECLIENT_HTACCESS_TITLE=".htaccess Maker"
COM_MOKOSUITECLIENT_TICKETS_TITLE="Helpdesk"
; ACL ; ACL
COM_MOKOSUITECLIENT_ACL_TITLE="Permissions"
COM_MOKOSUITECLIENT_ACL_DESC="Manage access permissions for MokoSuite component features."
COM_MOKOSUITECLIENT_ACL_DASHBOARD="View Dashboard" COM_MOKOSUITECLIENT_ACL_DASHBOARD="View Dashboard"
COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC="Allow viewing the MokoSuiteClient control panel dashboard." COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC="Allow viewing the MokoSuite control panel dashboard."
COM_MOKOSUITECLIENT_ACL_EXTENSIONS="Manage Extensions" COM_MOKOSUITECLIENT_ACL_EXTENSIONS="Manage Extensions"
COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions." COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
COM_MOKOSUITECLIENT_ACL_HTACCESS="Manage .htaccess"
COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
COM_MOKOSUITECLIENT_ACL_TICKETS="View Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE="Create Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN="Assign Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE="Toggle Plugins" COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE="Toggle Plugins"
COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuiteClient feature plugins." COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuite feature plugins."
COM_MOKOSUITECLIENT_ACL_CACHE="Clear Cache" COM_MOKOSUITECLIENT_ACL_CACHE="Clear Cache"
COM_MOKOSUITECLIENT_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard." COM_MOKOSUITECLIENT_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
COM_MOKOSUITECLIENT_ACL_WAFLOG="View WAF Log"
COM_MOKOSUITECLIENT_ACL_WAFLOG_DESC="Allow viewing the Web Application Firewall activity log."
COM_MOKOSUITECLIENT_ACL_IMPERSONATE="Impersonate Users"
COM_MOKOSUITECLIENT_ACL_IMPERSONATE_DESC="Allow logging into the frontend as another user for support purposes."
COM_MOKOSUITECLIENT_ACL_SNIPPETS="Manage Snippets"
COM_MOKOSUITECLIENT_ACL_SNIPPETS_DESC="Allow creating, editing, and deleting reusable content snippets."
COM_MOKOSUITECLIENT_ACL_TEMPLATES="Manage Content Templates"
COM_MOKOSUITECLIENT_ACL_TEMPLATES_DESC="Allow creating, editing, and deleting article content templates."
COM_MOKOSUITECLIENT_ACL_REPLACEMENTS="Manage Replacements"
COM_MOKOSUITECLIENT_ACL_REPLACEMENTS_DESC="Allow creating, editing, and deleting text replacement rules."
COM_MOKOSUITECLIENT_ACL_CONDITIONS="Manage Conditions"
COM_MOKOSUITECLIENT_ACL_CONDITIONS_DESC="Allow creating, editing, and deleting display condition sets for modules and content."
@@ -3,8 +3,8 @@
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
COM_MOKOSUITECLIENT="MokoSuite" COM_MOKOSUITECLIENT="MokoSuite"
COM_MOKOSUITECLIENT_DESCRIPTION="MokoSuiteClient admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management." COM_MOKOSUITECLIENT_DESCRIPTION="MokoSuite admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuiteClient Control Panel" COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuite Control Panel"
COM_MOKOSUITECLIENT_MENU_DASHBOARD="Dashboard" COM_MOKOSUITECLIENT_MENU_DASHBOARD="Dashboard"
COM_MOKOSUITECLIENT_MENU_EXTENSIONS="Moko Extensions" COM_MOKOSUITECLIENT_MENU_EXTENSIONS="Moko Extensions"
COM_MOKOSUITECLIENT_MENU_PLUGINS="Feature Plugins" COM_MOKOSUITECLIENT_MENU_PLUGINS="Feature Plugins"
@@ -227,3 +227,126 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
PRIMARY KEY (`dlid_hash`), PRIMARY KEY (`dlid_hash`),
KEY `idx_checked` (`checked_at`) KEY `idx_checked` (`checked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Conditions Engine — rule-based display conditions
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`hash` VARCHAR(32) NOT NULL DEFAULT '',
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_groups` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`condition_id` INT UNSIGNED NOT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_condition` (`condition_id`),
KEY `idx_ordering` (`condition_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_rules` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`group_id` INT UNSIGNED NOT NULL,
`type` VARCHAR(50) NOT NULL DEFAULT '',
`exclude` TINYINT(1) NOT NULL DEFAULT 0,
`params` TEXT NOT NULL,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_group` (`group_id`),
KEY `idx_type` (`type`),
KEY `idx_ordering` (`group_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_map` (
`condition_id` INT UNSIGNED NOT NULL,
`extension` VARCHAR(50) NOT NULL DEFAULT '',
`item_id` INT UNSIGNED NOT NULL DEFAULT 0,
UNIQUE KEY `idx_unique` (`condition_id`, `item_id`, `extension`),
KEY `idx_ext_item` (`extension`, `item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================
-- Snippets — reusable text/HTML blocks insertable via {snippet}
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_snippets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`content` MEDIUMTEXT NOT NULL,
`params` TEXT NOT NULL,
`published` TINYINT(1) NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_alias` (`alias`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================
-- ReReplacer — backend-managed string/regex replacement rules
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_replacements` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL DEFAULT '',
`search` TEXT NOT NULL,
`replace_value` TEXT NOT NULL,
`area` VARCHAR(20) NOT NULL DEFAULT 'both',
`regex` TINYINT(1) NOT NULL DEFAULT 0,
`casesensitive` TINYINT(1) NOT NULL DEFAULT 0,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`published` TINYINT(1) NOT NULL DEFAULT 0,
`description` TEXT NOT NULL,
`enable_in_admin` TINYINT(1) NOT NULL DEFAULT 0,
`color` VARCHAR(8) DEFAULT NULL,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Content Templates
--
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_content_templates` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`template_data` MEDIUMTEXT NOT NULL,
`joomla_category_id` INT NOT NULL DEFAULT 0,
`access` INT UNSIGNED NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`),
KEY `idx_category` (`joomla_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -24,19 +24,18 @@ class DisplayController extends BaseController
* ACL map: view name => required permission. * ACL map: view name => required permission.
*/ */
private const VIEW_ACL = [ private const VIEW_ACL = [
'dashboard' => 'mokosuiteclient.dashboard', 'dashboard' => 'mokosuiteclient.dashboard',
'extensions' => 'mokosuiteclient.extensions', 'extensions' => 'mokosuiteclient.extensions',
'htaccess' => 'mokosuiteclient.htaccess', 'htaccess' => 'mokosuiteclient.htaccess',
'tickets' => 'mokosuiteclient.tickets', 'privacy' => 'core.admin',
'ticket' => 'mokosuiteclient.tickets', 'waflog' => 'mokosuiteclient.security.waflog',
'privacy' => 'core.admin', 'automation' => 'core.admin',
'waflog' => 'core.admin', 'database' => 'core.admin',
'categories' => 'mokosuiteclient.tickets', 'cleanup' => 'mokosuiteclient.cache',
'canned' => 'mokosuiteclient.tickets', 'snippets' => 'mokosuiteclient.snippets.manage',
'automation' => 'core.admin', 'templates' => 'mokosuiteclient.templates.manage',
'database' => 'core.admin', 'replacements' => 'mokosuiteclient.replacements.manage',
'cleanup' => 'mokosuiteclient.cache', 'conditions' => 'mokosuiteclient.conditions.manage',
'ticketsettings' => 'core.admin',
]; ];
public function display($cachable = false, $urlparams = []) public function display($cachable = false, $urlparams = [])
@@ -142,6 +141,22 @@ class DisplayController extends BaseController
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: ''; $domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
$timestamp = time(); $timestamp = time();
// Discover all MokoSuite ecosystem packages for HQ
$mokoPackages = [];
try {
$pkgDb = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$pkgQuery = $pkgDb->getQuery(true)
->select([$pkgDb->quoteName('element'), $pkgDb->quoteName('manifest_cache')])
->from($pkgDb->quoteName('#__extensions'))
->where('(' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokosuite%')
. ' OR ' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokojoom%') . ')');
$pkgDb->setQuery($pkgQuery);
foreach ($pkgDb->loadObjectList() ?: [] as $pkg) {
$m = json_decode($pkg->manifest_cache ?? '{}');
$mokoPackages[$pkg->element] = $m->version ?? '';
}
} catch (\Throwable $e) {}
$payload = json_encode([ $payload = json_encode([
'token' => $healthToken, 'token' => $healthToken,
'domain' => $domain, 'domain' => $domain,
@@ -150,6 +165,7 @@ class DisplayController extends BaseController
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(), 'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'timestamp' => $timestamp, 'timestamp' => $timestamp,
'moko_packages' => $mokoPackages,
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
// RSA sign the request // RSA sign the request
@@ -204,7 +220,7 @@ class DisplayController extends BaseController
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15, CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYPEER => true,
]); ]);
$response = curl_exec($ch); $response = curl_exec($ch);
@@ -347,64 +363,7 @@ class DisplayController extends BaseController
$app->close(); $app->close();
} }
// ================================================================== $input = Factory::getApplication()->getInput();
// Tickets
// ==================================================================
public function createTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets.create'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->createTicket([
'subject' => $input->getString('subject', ''),
'body' => $input->getRaw('body', ''),
'priority' => $input->getString('priority', 'normal'),
'category_id' => $input->getInt('category_id', 0),
'contact_id' => $input->getInt('contact_id', 0),
'assign_users' => $input->get('assign_users', [], 'ARRAY'),
'assign_groups' => $input->get('assign_groups', [], 'ARRAY'),
'custom_fields' => $input->get('custom_fields', [], 'ARRAY'),
]));
}
public function addTicketReply()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->addReply(
$input->getInt('ticket_id', 0),
$input->getRaw('body', ''),
(bool) $input->getInt('is_internal', 0)
));
}
public function updateTicketStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->updateStatus( $this->jsonResponse($this->getModel('Tickets')->updateStatus(
$input->getInt('ticket_id', 0), $input->getInt('ticket_id', 0),
@@ -412,95 +371,10 @@ class DisplayController extends BaseController
)); ));
} }
// ==================================================================
// Ticket Settings — Status/Priority CRUD
// ==================================================================
public function saveStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->saveStatus([
'id' => $input->getInt('id', 0),
'title' => $input->getString('title', ''),
'alias' => $input->getString('alias', ''),
'color' => $input->getString('color', 'bg-secondary'),
'is_default' => $input->getInt('is_default', 0),
'is_closed' => $input->getInt('is_closed', 0),
'ordering' => $input->getInt('ordering', 0),
]));
}
public function deleteStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->jsonResponse($this->getModel('Tickets')->deleteStatus($id));
}
public function savePriority()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->savePriority([
'id' => $input->getInt('id', 0),
'title' => $input->getString('title', ''),
'alias' => $input->getString('alias', ''),
'color' => $input->getString('color', 'bg-secondary'),
'is_default' => $input->getInt('is_default', 0),
'ordering' => $input->getInt('ordering', 0),
]));
}
public function deletePriority()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$id = Factory::getApplication()->getInput()->getInt('id', 0); $id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->jsonResponse($this->getModel('Tickets')->deletePriority($id)); $this->jsonResponse($this->getModel('Tickets')->deletePriority($id));
} }
// ==================================================================
// KB Search
// ==================================================================
public function searchKb()
{
$query = Factory::getApplication()->getInput()->getString('q', '');
if (strlen($query) < 3)
{
$this->jsonResponse(['results' => []]);
return;
}
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
@@ -568,216 +442,83 @@ class DisplayController extends BaseController
$this->jsonResponse($model->cleanDirectory($dirKey)); $this->jsonResponse($model->cleanDirectory($dirKey));
} }
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->updateStatus(
$input->getInt('ticket_id', 0),
$input->getInt('status', 0)
));
}
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->jsonResponse($this->getModel('Tickets')->deletePriority($id));
}
try
{
$db = Factory::getDbo();
$escaped = $db->quote('%' . $db->escape($query, true) . '%');
$results = $db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('l.title'), $db->quoteName('l.url'), $db->quoteName('l.description')])
->from($db->quoteName('#__finder_links', 'l'))
->where($db->quoteName('l.published') . ' = 1')
->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
. ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
->order($db->quoteName('l.title') . ' ASC')
->setLimit(8)
)->loadObjectList() ?: [];
foreach ($results as $r)
{
$r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
}
$this->jsonResponse(['results' => $results]);
}
catch (\Throwable $e)
{
Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
$this->jsonResponse(['results' => [], 'error' => 'Search unavailable']);
}
}
// ================================================================== // ==================================================================
// Helpdesk CRUD (#137, #138, #139) // Maintenance (#127, #128)
// ================================================================== // ==================================================================
public function saveCategory() public function optimizeDb()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$id = $input->getInt('id', 0);
$data = (object) [
'title' => $input->getString('title', ''),
'alias' => \Joomla\CMS\Filter\OutputFilter::stringURLSafe($input->getString('title', '')),
'sla_response_minutes' => $input->getInt('sla_response_minutes', 480),
'sla_resolution_minutes' => $input->getInt('sla_resolution_minutes', 2880),
'auto_assign_user' => $input->getInt('auto_assign_user', 0) ?: null,
'published' => $input->getInt('published', 1),
];
if ($id) {
$data->id = $id;
$db->updateObject('#__mokosuiteclient_ticket_categories', $data, 'id');
} else {
$data->ordering = 0;
$db->insertObject('#__mokosuiteclient_ticket_categories', $data, 'id');
}
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
}
public function deleteCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
}
public function reorderCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
$db = Factory::getDbo();
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_categories') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
public function saveCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$data = (object) [
'title' => $input->getString('title', ''),
'body' => $input->getRaw('body', ''),
'category_id' => $input->getInt('category_id', 0) ?: null,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokosuiteclient_ticket_canned', $data, 'id'); }
else { $db->insertObject('#__mokosuiteclient_ticket_canned', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
}
public function deleteCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
}
public function reorderCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
$db = Factory::getDbo();
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_canned') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
public function uploadAttachment()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('ticket_id', 0);
$replyId = $input->getInt('reply_id', 0) ?: null;
if (!$ticketId) { $this->jsonResponse(['success' => false, 'message' => 'Missing ticket_id']); return; }
$files = $input->files->get('attachments', [], 'raw');
if (empty($files) || empty($files['name'])) { $this->jsonResponse(['success' => false, 'message' => 'No files uploaded']); return; }
$saved = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::upload($ticketId, $replyId, $files);
$this->jsonResponse(['success' => true, 'message' => count($saved) . ' file(s) uploaded', 'count' => count($saved)]);
}
public function downloadAttachment()
{
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_ticket_attachments')->where('id = ' . $id));
$att = $db->loadObject();
if (!$att) { throw new \RuntimeException('Attachment not found', 404); }
$path = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getAbsolutePath($att);
if (!file_exists($path)) { throw new \RuntimeException('File not found', 404); }
$app = Factory::getApplication();
$app->setHeader('Content-Type', $att->mimetype ?: 'application/octet-stream');
$safeName = str_replace(['"', "\r", "\n"], '', $att->filename);
$app->setHeader('Content-Disposition', 'attachment; filename="' . $safeName . '"');
$app->setHeader('Content-Length', (string) filesize($path));
$app->sendHeaders();
readfile($path);
$app->close();
}
public function deleteAttachment()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$ok = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::delete($id);
$this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']);
}
public function rateTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('ticket_id', 0);
$rating = $input->getInt('rating', 0);
$feedback = $input->getString('feedback', '');
if (!$ticketId || $rating < 1 || $rating > 5) {
$this->jsonResponse(['success' => false, 'message' => 'Invalid rating (1-5)']);
return;
}
$db = Factory::getDbo();
$db->setQuery(
'UPDATE ' . $db->quoteName('#__mokosuiteclient_tickets')
. ' SET satisfaction_rating = ' . $rating
. ', satisfaction_feedback = ' . $db->quote($feedback)
. ', satisfaction_rated_at = ' . $db->quote(Factory::getDate()->toSql())
. ' WHERE id = ' . $ticketId
)->execute();
$this->jsonResponse(['success' => true, 'message' => 'Thank you for your feedback!']);
}
public function saveAutomation()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput(); $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
$db = Factory::getDbo(); $this->jsonResponse($model->optimizeTables());
$data = (object) [
'title' => $input->getString('title', ''),
'trigger_event' => $input->getString('trigger_event', 'ticket_created'),
'conditions' => $input->getRaw('conditions', '[]'),
'actions' => $input->getRaw('actions', '[]'),
'behavior' => $input->getString('behavior', 'append'),
'enabled' => 1,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokosuiteclient_ticket_automation', $data, 'id'); }
else { $db->insertObject('#__mokosuiteclient_ticket_automation', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
} }
public function deleteAutomation() public function repairDb()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$db = Factory::getDbo(); $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $this->jsonResponse($model->repairTables());
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
} }
public function toggleAutomation() public function purgeSessions()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput(); $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
$db = Factory::getDbo(); $this->jsonResponse($model->purgeSessions());
$db->setQuery($db->getQuery(true)->update('#__mokosuiteclient_ticket_automation')
->set('enabled = ' . $input->getInt('enabled', 0))
->where('id = ' . $input->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
} }
public function reorderAutomation() public function cleanDirectory()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } if (!$this->checkAcl('mokosuiteclient.cache')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true); $dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; } $model = new \Moko\Component\MokoSuiteClient\Administrator\Model\MaintenanceModel();
$db = Factory::getDbo(); $this->jsonResponse($model->cleanDirectory($dirKey));
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_automation') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
} }
// ================================================================== // ==================================================================
@@ -891,7 +632,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) if (!$this->checkAcl('mokosuiteclient.security.waflog'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -907,7 +648,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) if (!$this->checkAcl('mokosuiteclient.security.waflog'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -991,19 +732,6 @@ class DisplayController extends BaseController
// Importers // Importers
// ================================================================== // ==================================================================
public function importAts()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAts());
}
public function importAdminTools() public function importAdminTools()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
@@ -0,0 +1,524 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
/**
* Conditions Engine — evaluates rule-based display conditions.
*
* Supports nested groups of rules with AND/OR logic and per-rule exclusion.
*
* @since 02.48.00
*/
class ConditionsHelper
{
/**
* Runtime evaluation cache keyed by condition ID.
*
* @var array<int, bool>
*/
private static array $cache = [];
/**
* Check whether a condition set passes.
*
* @param int $conditionId The condition record ID.
*
* @return bool True when the condition passes (content should display).
*/
public static function pass(int $conditionId): bool
{
if (isset(self::$cache[$conditionId])) {
return self::$cache[$conditionId];
}
$condition = self::load($conditionId);
if ($condition === null || !(int) $condition->published) {
self::$cache[$conditionId] = false;
return false;
}
$groups = $condition->groups ?? [];
if (empty($groups)) {
// No groups means no restrictions — pass.
self::$cache[$conditionId] = true;
return true;
}
$matchAll = (bool) $condition->match_all;
foreach ($groups as $group) {
$groupResult = self::passGroup($group);
if ($matchAll && !$groupResult) {
self::$cache[$conditionId] = false;
return false;
}
if (!$matchAll && $groupResult) {
self::$cache[$conditionId] = true;
return true;
}
}
// match_all: all passed; match_any: none passed.
$result = $matchAll;
self::$cache[$conditionId] = $result;
return $result;
}
/**
* Load a condition with its groups and rules from the database.
*
* @param int $conditionId The condition record ID.
*
* @return object|null The condition object with nested groups/rules, or null.
*/
public static function load(int $conditionId): ?object
{
$db = Factory::getContainer()->get('DatabaseDriver');
// Load the condition record.
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $conditionId, \Joomla\Database\ParameterType::INTEGER);
$condition = $db->setQuery($query)->loadObject();
if ($condition === null) {
return null;
}
// Load groups.
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions_groups'))
->where($db->quoteName('condition_id') . ' = :cid')
->bind(':cid', $conditionId, \Joomla\Database\ParameterType::INTEGER)
->order($db->quoteName('ordering') . ' ASC');
$groups = $db->setQuery($query)->loadObjectList();
// Load rules for each group.
foreach ($groups as $group) {
$groupId = (int) $group->id;
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions_rules'))
->where($db->quoteName('group_id') . ' = :gid')
->bind(':gid', $groupId, \Joomla\Database\ParameterType::INTEGER)
->order($db->quoteName('ordering') . ' ASC');
$group->rules = $db->setQuery($query)->loadObjectList();
// Decode params JSON on each rule.
foreach ($group->rules as $rule) {
$rule->params = json_decode($rule->params ?: '{}');
}
}
$condition->groups = $groups;
return $condition;
}
/**
* Evaluate a single group (AND/OR its rules).
*
* @param object $group The group object with a rules array.
*
* @return bool
*/
private static function passGroup(object $group): bool
{
$rules = $group->rules ?? [];
if (empty($rules)) {
return true;
}
$matchAll = (bool) $group->match_all;
foreach ($rules as $rule) {
$ruleResult = self::passRule($rule);
// If the rule is an exclusion, invert the result.
if ((int) $rule->exclude) {
$ruleResult = !$ruleResult;
}
if ($matchAll && !$ruleResult) {
return false;
}
if (!$matchAll && $ruleResult) {
return true;
}
}
return $matchAll;
}
/**
* Evaluate a single rule by dispatching to the right type handler.
*
* @param object $rule The rule object (type, params decoded).
*
* @return bool
*/
private static function passRule(object $rule): bool
{
$params = $rule->params ?? new \stdClass();
return match ($rule->type) {
'menu__menu_item' => self::evalMenuMenuItem($params),
'menu__home_page' => self::evalMenuHomePage($params),
'visitor__user_group' => self::evalVisitorUserGroup($params),
'visitor__access_level' => self::evalVisitorAccessLevel($params),
'date__date' => self::evalDateDate($params),
'date__day' => self::evalDateDay($params),
'other__url' => self::evalOtherUrl($params),
default => false,
};
}
// ------------------------------------------------------------------
// Rule type evaluators
// ------------------------------------------------------------------
/**
* menu__menu_item — check if current menu item ID is in selection.
*/
private static function evalMenuMenuItem(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$app = Factory::getApplication();
$itemId = (int) $app->getInput()->getInt('Itemid', 0);
return \in_array($itemId, $selection, true);
}
/**
* menu__home_page — check if current page is the site home page.
*/
private static function evalMenuHomePage(object $params): bool
{
$app = Factory::getApplication();
$menu = $app->getMenu();
if ($menu === null) {
return false;
}
$active = $menu->getActive();
$default = $menu->getDefault($app->getLanguage()->getTag());
$isHome = ($active !== null && $default !== null && $active->id === $default->id);
// params->selection can be [1] for "is home" or [0] for "is not home".
$want = (bool) ($params->selection[0] ?? true);
return $isHome === $want;
}
/**
* visitor__user_group — check if current user belongs to specified groups.
*/
private static function evalVisitorUserGroup(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$user = Factory::getApplication()->getIdentity();
$userGroups = $user ? $user->getAuthorisedGroups() : [];
$comparison = $params->comparison ?? 'any';
if ($comparison === 'all') {
return empty(array_diff($selection, $userGroups));
}
// Default: any
return !empty(array_intersect($selection, $userGroups));
}
/**
* visitor__access_level — check if current user has specified access levels.
*/
private static function evalVisitorAccessLevel(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$user = Factory::getApplication()->getIdentity();
$accessLevels = $user ? $user->getAuthorisedViewLevels() : [];
$comparison = $params->comparison ?? 'any';
if ($comparison === 'all') {
return empty(array_diff($selection, $accessLevels));
}
return !empty(array_intersect($selection, $accessLevels));
}
/**
* date__date — check if current date is before/after/between specified dates.
*
* params->comparison: 'before', 'after', 'between'
* params->selection: [start_date] or [start_date, end_date]
*/
private static function evalDateDate(object $params): bool
{
$comparison = $params->comparison ?? 'after';
$selection = (array) ($params->selection ?? []);
if (empty($selection)) {
return true;
}
$now = Factory::getDate()->toUnix();
return match ($comparison) {
'before' => $now < strtotime($selection[0]),
'after' => $now > strtotime($selection[0]),
'between' => isset($selection[1])
&& $now >= strtotime($selection[0])
&& $now <= strtotime($selection[1]),
default => false,
};
}
/**
* date__day — check if current day of week matches selection.
*
* params->selection: array of day numbers (1=Monday .. 7=Sunday, ISO-8601).
*/
private static function evalDateDay(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$today = (int) Factory::getDate()->format('N'); // 1=Mon, 7=Sun
return \in_array($today, $selection, true);
}
/**
* other__url — check if current URL matches a regex pattern.
*
* params->selection: array of regex patterns (without delimiters).
*/
private static function evalOtherUrl(object $params): bool
{
$patterns = (array) ($params->selection ?? []);
if (empty($patterns)) {
return true;
}
$url = Uri::getInstance()->toString();
foreach ($patterns as $pattern) {
$pattern = trim($pattern);
if ($pattern === '') {
continue;
}
// Wrap in delimiters, escape internal delimiters if needed.
if (@preg_match('#' . $pattern . '#i', $url)) {
return true;
}
}
return false;
}
// ------------------------------------------------------------------
// Mapping helpers
// ------------------------------------------------------------------
/**
* Get all condition IDs mapped to a specific extension/item pair.
*
* @param string $extension The extension identifier (e.g. 'mod_custom').
* @param int $itemId The item ID within that extension.
*
* @return int[] Array of condition IDs.
*/
public static function getConditionsForItem(string $extension, int $itemId): array
{
$db = Factory::getContainer()->get('DatabaseDriver');
$query = $db->getQuery(true)
->select($db->quoteName('condition_id'))
->from($db->quoteName('#__mokosuiteclient_conditions_map'))
->where($db->quoteName('extension') . ' = :ext')
->where($db->quoteName('item_id') . ' = :iid')
->bind(':ext', $extension)
->bind(':iid', $itemId, \Joomla\Database\ParameterType::INTEGER);
return $db->setQuery($query)->loadColumn();
}
/**
* Check if an item should display based on its mapped conditions.
*
* If no conditions are mapped, the item displays (returns true).
* If conditions are mapped, ALL must pass for the item to display.
*
* @param string $extension The extension identifier.
* @param int $itemId The item ID.
*
* @return bool
*/
public static function shouldDisplay(string $extension, int $itemId): bool
{
$conditionIds = self::getConditionsForItem($extension, $itemId);
if (empty($conditionIds)) {
return true;
}
foreach ($conditionIds as $conditionId) {
if (!self::pass((int) $conditionId)) {
return false;
}
}
return true;
}
/**
* Evaluate a condition by its alias string.
*
* @param string $alias The condition alias.
*
* @return bool True when the condition passes.
*
* @since 02.48.00
*/
public static function passByAlias(string $alias): bool
{
$id = self::resolveAlias($alias);
if ($id === null) {
return false;
}
return self::pass($id);
}
/**
* Resolve a condition reference that may be an integer ID or an alias string.
*
* @param string $ref The reference (numeric ID or alias).
*
* @return int|null The condition ID, or null if not found.
*
* @since 02.48.00
*/
public static function resolveAlias(string $ref): ?int
{
if (is_numeric($ref)) {
return (int) $ref;
}
$db = Factory::getContainer()->get('DatabaseDriver');
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokosuiteclient_conditions'))
->where($db->quoteName('alias') . ' = :alias')
->bind(':alias', $ref);
$id = $db->setQuery($query)->loadResult();
return $id !== null ? (int) $id : null;
}
/**
* Evaluate a single inline rule (public wrapper around passRule).
*
* @param string $type The rule type (e.g. 'visitor__access_level').
* @param object $params The rule params object.
*
* @return bool
*
* @since 02.48.00
*/
public static function evaluateInlineRule(string $type, object $params): bool
{
$rule = (object) [
'type' => $type,
'params' => $params,
];
return self::passRule($rule);
}
/**
* Clear the evaluation cache (useful between requests in testing).
*
* @return void
*/
public static function clearCache(): void
{
self::$cache = [];
}
// ------------------------------------------------------------------
// Internal utilities
// ------------------------------------------------------------------
/**
* Normalize a mixed selection value into an array of integers.
*
* @param mixed $value Scalar, array, or object.
*
* @return int[]
*/
private static function toIntArray(mixed $value): array
{
if (\is_object($value)) {
$value = (array) $value;
}
if (!\is_array($value)) {
$value = [$value];
}
return array_map('intval', array_values($value));
}
}
@@ -213,30 +213,46 @@ class DashboardModel extends BaseDatabaseModel
} }
/** /**
* Get installed MokoSuiteClient component and modules with versions. * Discover all installed MokoSuite ecosystem extensions.
* *
* @return array Array of extension objects with name, element, type, version. * Fuzzy-matches packages, components, modules, plugins, and libraries
* by element name containing "mokosuite", "mokosuiteclient", "mokojoom",
* or "moko" prefix patterns.
*
* @return array Extension objects with name, element, type, version, enabled, family.
*/ */
public function getMokoExtensions(): array public function getMokoExtensions(): array
{ {
$db = $this->getDatabase(); $db = $this->getDatabase();
$el = $db->quoteName('element');
// Fuzzy match: any extension whose element contains moko patterns
$patterns = [
$el . ' LIKE ' . $db->quote('pkg_mokosuite%'),
$el . ' LIKE ' . $db->quote('com_mokosuite%'),
$el . ' LIKE ' . $db->quote('mod_mokosuite%'),
$el . ' LIKE ' . $db->quote('mokosuite%'),
$el . ' LIKE ' . $db->quote('mokosuiteclient%'),
$el . ' LIKE ' . $db->quote('pkg_mokojoom%'),
$el . ' LIKE ' . $db->quote('com_mokojoom%'),
$el . ' LIKE ' . $db->quote('mod_mokojoom%'),
$el . ' LIKE ' . $db->quote('mokojoom%'),
$el . ' LIKE ' . $db->quote('plg_%_mokosuite%'),
$el . ' LIKE ' . $db->quote('plg_%_mokojoom%'),
];
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select([ ->select([
$db->quoteName('extension_id'),
$db->quoteName('element'), $db->quoteName('element'),
$db->quoteName('name'), $db->quoteName('name'),
$db->quoteName('type'), $db->quoteName('type'),
$db->quoteName('folder'),
$db->quoteName('enabled'), $db->quoteName('enabled'),
$db->quoteName('manifest_cache'), $db->quoteName('manifest_cache'),
]) ])
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where('(' ->where('(' . implode(' OR ', $patterns) . ')')
// The component
. '(' . $db->quoteName('type') . ' = ' . $db->quote('component')
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient') . ')'
// Admin modules
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuiteclient%') . ')'
. ')')
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC'); ->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
$db->setQuery($query); $db->setQuery($query);
@@ -248,12 +264,27 @@ class DashboardModel extends BaseDatabaseModel
{ {
$manifest = json_decode($row->manifest_cache ?? '{}'); $manifest = json_decode($row->manifest_cache ?? '{}');
// Determine product family from element name
$family = 'mokosuite';
if (stripos($row->element, 'mokosuiteclient') !== false) {
$family = 'mokosuiteclient';
} elseif (stripos($row->element, 'mokosuitehq') !== false) {
$family = 'mokosuitehq';
} elseif (stripos($row->element, 'mokosuitecrm') !== false) {
$family = 'mokosuitecrm';
} elseif (stripos($row->element, 'mokojoom') !== false) {
$family = 'mokojoom';
}
$extensions[] = (object) [ $extensions[] = (object) [
'element' => $row->element, 'extension_id' => (int) $row->extension_id,
'name' => $manifest->name ?? $row->name, 'element' => $row->element,
'type' => $row->type, 'name' => $manifest->name ?? $row->name,
'version' => $manifest->version ?? '', 'type' => $row->type,
'enabled' => (int) $row->enabled, 'folder' => $row->folder ?? '',
'version' => $manifest->version ?? '',
'enabled' => (int) $row->enabled,
'family' => $family,
]; ];
} }
@@ -277,7 +308,9 @@ class DashboardModel extends BaseDatabaseModel
->select([$db->quoteName('element'), $db->quoteName('protected')]) ->select([$db->quoteName('element'), $db->quoteName('protected')])
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $extensionId) ->where($db->quoteName('extension_id') . ' = ' . $extensionId)
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient')
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuiteclient\\_%') . ')');
$db->setQuery($query); $db->setQuery($query);
$ext = $db->loadObject(); $ext = $db->loadObject();
@@ -454,7 +487,7 @@ class DashboardModel extends BaseDatabaseModel
]) ])
->from($db->quoteName('#__action_logs', 'a')) ->from($db->quoteName('#__action_logs', 'a'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id')) ->leftJoin($db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id'))
->where($db->quoteName('a.message_language_key') . ' LIKE ' . $db->quote('%LOGIN%')) ->where($db->quoteName('a.message_language_key') . ' = ' . $db->quote('PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'))
->order($db->quoteName('a.log_date') . ' DESC') ->order($db->quoteName('a.log_date') . ' DESC')
->setLimit($limit); ->setLimit($limit);
$db->setQuery($query); $db->setQuery($query);
@@ -568,7 +601,7 @@ class DashboardModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
"SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total" "SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__mokosuiteclient_waf_log') . " FROM " . $db->quoteName('#__mokosuiteclient_waf_log')
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)" . " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL " . (int) $days . " DAY)"
. " GROUP BY day ORDER BY day" . " GROUP BY day ORDER BY day"
); );
$rows = $db->loadObjectList() ?: []; $rows = $db->loadObjectList() ?: [];
@@ -609,7 +642,7 @@ class DashboardModel extends BaseDatabaseModel
"SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total" "SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__action_logs') . " FROM " . $db->quoteName('#__action_logs')
. " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'" . " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'"
. " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)" . " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL " . (int) $days . " DAY)"
. " GROUP BY day ORDER BY day" . " GROUP BY day ORDER BY day"
); );
$rows = $db->loadObjectList() ?: []; $rows = $db->loadObjectList() ?: [];
@@ -238,8 +238,15 @@ class ExtensionsModel extends BaseDatabaseModel
return []; return [];
} }
// Determine site's update channel preference // Dev channel only available on Moko domains; all others forced to stable
$channel = 'dev'; // default to dev — show everything $isMokoDomain = (bool) preg_match('/\.mokoconsulting\.tech$/i', $_SERVER['HTTP_HOST'] ?? '');
$channel = 'stable';
if ($isMokoDomain) {
try {
$channel = \Joomla\CMS\Component\ComponentHelper::getParams('com_installer')
->get('update_channel', 'stable') ?: 'stable';
} catch (\Throwable $e) {}
}
$hasStable = false; $hasStable = false;
$hasDev = false; $hasDev = false;
@@ -269,7 +276,18 @@ class ExtensionsModel extends BaseDatabaseModel
$hasDev = true; $hasDev = true;
} }
if ($ver === '' || version_compare($ver, $bestVersion, '<=')) if ($ver === '')
{
continue;
}
// Respect update channel: stable channel skips dev-tagged versions
if ($channel === 'stable' && $tag === 'dev')
{
continue;
}
if (version_compare($ver, $bestVersion, '<='))
{ {
continue; continue;
} }
File diff suppressed because it is too large Load Diff
@@ -1,183 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class AttachmentService
{
private const STORAGE_DIR = JPATH_ROOT . '/media/com_mokosuiteclient/attachments';
private const ALLOWED_EXTENSIONS = [
'jpg', 'jpeg', 'png', 'gif', 'webp',
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'rtf',
'zip', 'gz', 'tar',
];
private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
/**
* Upload file(s) for a ticket or reply.
*
* @param int $ticketId Ticket ID
* @param int|null $replyId Reply ID (null for ticket-level attachments)
* @param array $files $_FILES array entry (single or multi)
* @return array Saved attachment records
*/
public static function upload(int $ticketId, ?int $replyId, array $files): array
{
$saved = [];
// Normalize single file to array format
if (!is_array($files['name'])) {
$files = [
'name' => [$files['name']],
'type' => [$files['type']],
'tmp_name' => [$files['tmp_name']],
'error' => [$files['error']],
'size' => [$files['size']],
];
}
$ticketDir = self::STORAGE_DIR . '/' . $ticketId;
if (!is_dir($ticketDir) && !Folder::create($ticketDir)) {
Log::add("Failed to create attachment directory: {$ticketDir}", Log::ERROR, 'mokosuiteclient');
return [];
}
$userId = (int) Factory::getUser()->id;
$db = Factory::getDbo();
for ($i = 0, $count = count($files['name']); $i < $count; $i++)
{
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
Log::add("Attachment upload error for '{$files['name'][$i]}': PHP error code {$files['error'][$i]}", Log::WARNING, 'mokosuiteclient');
continue;
}
$originalName = File::makeSafe($files['name'][$i]);
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// Validate extension
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
Log::add("Attachment rejected: disallowed extension .{$ext}", Log::WARNING, 'mokosuiteclient');
continue;
}
// Validate size
if ($files['size'][$i] > self::MAX_FILE_SIZE) {
Log::add("Attachment rejected: file too large ({$files['size'][$i]} bytes)", Log::WARNING, 'mokosuiteclient');
continue;
}
// Generate unique filename to prevent overwrites
$storedName = uniqid('att_', true) . '.' . $ext;
$destPath = $ticketDir . '/' . $storedName;
if (!File::upload($files['tmp_name'][$i], $destPath)) {
Log::add("Attachment upload failed: {$originalName}", Log::ERROR, 'mokosuiteclient');
continue;
}
$record = (object) [
'ticket_id' => $ticketId,
'reply_id' => $replyId,
'filename' => $originalName,
'filepath' => $ticketId . '/' . $storedName,
'filesize' => $files['size'][$i],
'mimetype' => mime_content_type($destPath) ?: 'application/octet-stream',
'uploaded_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_attachments', $record, 'id');
$saved[] = $record;
}
return $saved;
}
/**
* Get attachments for a ticket.
*/
public static function getForTicket(int $ticketId): array
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('a.*, u.name AS uploader_name')
->from($db->quoteName('#__mokosuiteclient_ticket_attachments', 'a'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = a.uploaded_by')
->where($db->quoteName('a.ticket_id') . ' = ' . $ticketId)
->order('a.created ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get the absolute filesystem path for an attachment.
*/
public static function getAbsolutePath(object $attachment): ?string
{
$path = realpath(self::STORAGE_DIR . '/' . $attachment->filepath);
if ($path === false || !str_starts_with($path, realpath(self::STORAGE_DIR))) {
return null;
}
return $path;
}
/**
* Delete an attachment (file + DB record).
*/
public static function delete(int $attachmentId): bool
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__mokosuiteclient_ticket_attachments')
->where('id = ' . $attachmentId)
);
$att = $db->loadObject();
if (!$att) {
return false;
}
$path = self::STORAGE_DIR . '/' . $att->filepath;
if (file_exists($path)) {
File::delete($path);
}
$db->setQuery(
$db->getQuery(true)
->delete('#__mokosuiteclient_ticket_attachments')
->where('id = ' . $attachmentId)
)->execute();
return true;
}
/**
* Format file size for display.
*/
public static function formatSize(int $bytes): string
{
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
return round($bytes / 1048576, 1) . ' MB';
}
}
@@ -1,280 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Automation rule engine — evaluates trigger/condition/action rules.
*
* Called from event hooks (system plugin, task plugin) whenever
* a triggering event occurs. Loads matching rules, checks conditions,
* and executes actions.
*
* @since 02.35.00
*/
class AutomationEngine
{
/**
* Fire all matching rules for a given trigger event.
*
* @param string $triggerEvent Event name (ticket_created, user_login, etc.)
* @param array $context Context data (ticket object, user data, etc.)
*/
public static function fire(string $triggerEvent, array $context = []): void
{
try
{
$rules = self::getActiveRules($triggerEvent);
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if (self::evaluateConditions($conditions, $context))
{
self::executeActions($actions, $rule, $context);
}
}
}
catch (\Throwable $e)
{
Log::add('Automation engine error: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
}
}
/**
* Get active automation rules for a trigger event.
*/
private static function getActiveRules(string $event): array
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__mokosuiteclient_ticket_automation')
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order('ordering ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Evaluate all conditions (AND logic).
*/
private static function evaluateConditions(array $conditions, array $context): bool
{
foreach ($conditions as $c)
{
$field = $c['field'] ?? '';
$op = $c['op'] ?? 'eq';
$expected = $c['value'] ?? '';
$actual = $context[$field] ?? '';
switch ($op)
{
case 'eq': if ((string) $actual !== (string) $expected) return false; break;
case 'neq': if ((string) $actual === (string) $expected) return false; break;
case 'gt': if ((float) $actual <= (float) $expected) return false; break;
case 'lt': if ((float) $actual >= (float) $expected) return false; break;
case 'in':
$values = array_map('trim', explode(',', $expected));
if (!in_array((string) $actual, $values, true)) return false;
break;
case 'not_in':
$values = array_map('trim', explode(',', $expected));
if (in_array((string) $actual, $values, true)) return false;
break;
}
}
return true;
}
/**
* Execute actions for a matched rule.
*/
private static function executeActions(array $actions, object $rule, array $context): void
{
$db = Factory::getDbo();
$ticketId = (int) ($context['ticket_id'] ?? $context['id'] ?? 0);
foreach ($actions as $action)
{
$type = $action['type'] ?? '';
$value = $action['value'] ?? '';
try
{
switch ($type)
{
case 'set_status':
if ($ticketId) {
$statusId = self::resolveStatusId($db, $value);
$sets = "status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($statusId) { $sets .= ", status_id = {$statusId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'set_priority':
if ($ticketId) {
$priorityId = self::resolvePriorityId($db, $value);
$sets = "priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($priorityId) { $sets .= ", priority_id = {$priorityId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'assign':
$assignId = (int) $value;
if ($ticketId && $assignId > 0) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET assigned_to = {$assignId}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
}
break;
case 'add_note':
if ($ticketId) {
$note = (object) [
'ticket_id' => $ticketId,
'user_id' => 0,
'body' => $value ?: '[Automation: ' . ($rule->title ?? '') . ']',
'is_internal' => 1,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $note);
}
break;
case 'send_email':
NotificationService::securityAlert(
'automation',
'Automation: ' . ($rule->title ?? ''),
$value ?: 'Rule triggered for ticket #' . $ticketId
);
break;
case 'send_ntfy':
NotificationService::pushNtfySecurity(
'automation',
'Automation: ' . ($rule->title ?? ''),
$value ?: 'Rule triggered for ticket #' . $ticketId
);
break;
case 'close':
if ($ticketId) {
$closedId = self::resolveClosedStatusId($db);
$sets = "status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($closedId) { $sets .= ", status_id = {$closedId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'create_ticket':
self::createTicketFromAutomation($rule, $context, $value);
break;
}
}
catch (\Throwable $e)
{
Log::add("Automation action '{$type}' failed for rule #{$rule->id}: " . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
}
}
}
/**
* Create a ticket from automation (with behavior: append/always_new/skip_if_open).
*/
private static function resolveStatusId($db, string $alias): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
)->loadResult();
}
private static function resolvePriorityId($db, string $alias): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
)->loadResult();
}
private static function resolveClosedStatusId($db): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('is_closed') . ' = 1'), 0, 1
)->loadResult();
}
private static function createTicketFromAutomation(object $rule, array $context, string $subject): void
{
$db = Factory::getDbo();
$behavior = $rule->behavior ?? 'append';
$userId = (int) ($context['user_id'] ?? 0);
$catId = (int) ($context['category_id'] ?? 0);
if ($behavior !== 'always_new' && $userId > 0)
{
// Check for existing open ticket (check both status ENUM and status_id)
$query = $db->getQuery(true)
->select('t.id')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->join('LEFT', $db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON t.status_id = s.id')
->where('t.created_by = ' . $userId)
->where("(s.id IS NULL AND t.status NOT IN ('closed', 'resolved')) OR (s.id IS NOT NULL AND s.is_closed = 0)");
if ($catId > 0) {
$query->where('category_id = ' . $catId);
}
$db->setQuery($query, 0, 1);
$existingId = (int) $db->loadResult();
if ($existingId > 0)
{
if ($behavior === 'skip_if_open') return;
// append — add reply to existing ticket
$reply = (object) [
'ticket_id' => $existingId,
'user_id' => 0,
'body' => $subject ?: '[Automation: ' . ($rule->title ?? '') . ']',
'is_internal' => 1,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply);
return;
}
}
// Create new ticket
$openStatusId = self::resolveStatusId($db, 'open') ?: null;
$normalPriorityId = self::resolvePriorityId($db, $context['priority'] ?? 'normal') ?: null;
$ticket = (object) [
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
'body' => $context['body'] ?? '',
'status' => 'open',
'status_id' => $openStatusId,
'priority' => $context['priority'] ?? 'normal',
'priority_id' => $normalPriorityId,
'category_id' => $catId ?: null,
'created_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
}
}
@@ -1,581 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Helpdesk email notification service.
*
* Sends emails for ticket events to Joomla users (by ID) and/or
* raw email addresses. Uses Joomla's configured mailer.
*
* @since 02.32.00
*/
class NotificationService
{
/**
* Send a ticket notification email.
*
* @param string $event Event name (ticket_created, ticket_replied, status_changed, ticket_assigned)
* @param object $ticket Ticket object with id, subject, status, priority, created_by, assigned_to
* @param array $extra Extra context (reply body, old status, etc.)
*/
public static function notify(string $event, object $ticket, array $extra = []): void
{
try
{
$recipients = self::getRecipients($event, $ticket);
if (empty($recipients))
{
return;
}
$subject = self::buildSubject($event, $ticket);
$body = self::buildBody($event, $ticket, $extra);
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($subject);
$mailer->setBody($body);
foreach ($recipients as $email)
{
$email = trim($email);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
{
continue;
}
try
{
$mailer->clearAddresses();
$mailer->addRecipient($email);
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
// Push notification via ntfy
self::pushNtfy($event, $ticket, $subject);
}
catch (\Throwable $e)
{
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Determine recipients based on event type and ticket data.
*/
private static function getRecipients(string $event, object $ticket): array
{
$emails = [];
// Get notification config from component params
$config = self::getNotificationConfig();
// Always notify configured admin emails
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$emails = array_merge($emails, $adminEmails);
// Always notify configured admin user IDs
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$emails[] = $email;
}
}
switch ($event)
{
case 'ticket_created':
// Notify assigned user if any
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_replied':
// Notify ticket creator (customer gets notified of staff reply)
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
// Notify assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'status_changed':
// Notify ticket creator
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_assigned':
// Notify newly assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
}
return array_unique($emails);
}
/**
* Build email subject line.
*/
private static function buildSubject(string $event, object $ticket): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$prefix = '[' . $siteName . ' #' . $ticket->id . '] ';
switch ($event)
{
case 'ticket_created':
return $prefix . 'New Ticket: ' . ($ticket->subject ?? '');
case 'ticket_replied':
return $prefix . 'Reply: ' . ($ticket->subject ?? '');
case 'status_changed':
return $prefix . 'Status Changed: ' . ($ticket->subject ?? '');
case 'ticket_assigned':
return $prefix . 'Assigned: ' . ($ticket->subject ?? '');
default:
return $prefix . ($ticket->subject ?? '');
}
}
/**
* Build email body.
*/
private static function buildBody(string $event, object $ticket, array $extra): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/index.php?option=com_mokosuiteclient&view=ticket&id=' . $ticket->id;
$lines = [];
$lines[] = $siteName . ' Support';
$lines[] = str_repeat('-', 40);
$lines[] = '';
switch ($event)
{
case 'ticket_created':
$lines[] = 'A new support ticket has been created.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = 'Category: ' . ($ticket->category_title ?? 'General');
$lines[] = '';
if (!empty($ticket->body))
{
$lines[] = 'Description:';
$lines[] = strip_tags($ticket->body);
$lines[] = '';
}
break;
case 'ticket_replied':
$lines[] = 'A new reply has been added to your ticket.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
$lines[] = '';
if (!empty($extra['reply_body']))
{
$lines[] = 'Reply:';
$lines[] = strip_tags($extra['reply_body']);
$lines[] = '';
}
break;
case 'status_changed':
$lines[] = 'Your ticket status has been updated.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'New Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
if (!empty($extra['old_status']))
{
$lines[] = 'Old Status: ' . ucwords(str_replace('_', ' ', $extra['old_status']));
}
$lines[] = '';
break;
case 'ticket_assigned':
$lines[] = 'A ticket has been assigned to you.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = '';
break;
}
$lines[] = 'View ticket: ' . $ticketUrl;
$lines[] = '';
$lines[] = '-- ';
$lines[] = $siteName . ' | Powered by MokoSuiteClient';
return implode("\n", $lines);
}
/**
* Get email address for a Joomla user ID.
*/
private static function getUserEmail(int $userId): ?string
{
if ($userId <= 0)
{
return null;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('email'))
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
);
return $db->loadResult() ?: null;
}
catch (\Throwable $e)
{
Log::add('Failed to look up email for user ID ' . $userId . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
return null;
}
}
/**
* Get notification configuration from component params.
*/
private static function getNotificationConfig(): array
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = json_decode($db->loadResult() ?? '{}', true);
return $params['notifications'] ?? [];
}
catch (\Throwable $e)
{
Log::add('Failed to load notification config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
return [];
}
}
// ==================================================================
// Ntfy Push Notifications (#205)
// ==================================================================
/**
* Send a push notification via ntfy for ticket events.
*/
private static function pushNtfy(string $event, object $ticket, string $title): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_topic'] ?? 'mokosuiteclient-tickets';
$ntfyToken = $config['ntfy_token'] ?? '';
$tagMap = [
'ticket_created' => 'ticket,new',
'ticket_replied' => 'speech_balloon',
'status_changed' => 'arrows_counterclockwise',
'ticket_assigned' => 'bust_in_silhouette',
];
$priorityMap = [
'ticket_created' => '4',
'ticket_replied' => '3',
'status_changed' => '3',
'ticket_assigned' => '3',
];
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/administrator/index.php?option=com_mokosuiteclient&view=ticket&id=' . ($ticket->id ?? 0);
$message = self::buildNtfyMessage($event, $ticket);
$headers = [
'Title: ' . $title,
'Priority: ' . ($priorityMap[$event] ?? '3'),
'Tags: ' . ($tagMap[$event] ?? 'ticket'),
'Click: ' . $ticketUrl,
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false)
{
Log::add("Ntfy push connection failed for event {$event}: " . $curlError, Log::WARNING, 'mokosuiteclient');
}
elseif ($httpCode < 200 || $httpCode >= 300)
{
Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuiteclient');
}
}
catch (\Throwable $e)
{
Log::add('Ntfy push error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Build a short ntfy message body for ticket events.
*/
private static function buildNtfyMessage(string $event, object $ticket): string
{
$subject = $ticket->subject ?? 'Ticket #' . ($ticket->id ?? '?');
switch ($event)
{
case 'ticket_created':
$priority = ucfirst($ticket->priority ?? 'normal');
return "New ticket: {$subject}\nPriority: {$priority}";
case 'ticket_replied':
return "Reply on: {$subject}";
case 'status_changed':
$status = ucwords(str_replace('_', ' ', $ticket->status ?? ''));
return "Status → {$status}: {$subject}";
case 'ticket_assigned':
return "Assigned to you: {$subject}";
default:
return $subject;
}
}
/**
* Send a push notification via ntfy for security events.
*/
public static function pushNtfySecurity(string $event, string $title, string $body): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_security_topic'] ?? $config['ntfy_topic'] ?? 'mokosuiteclient-security';
$ntfyToken = $config['ntfy_token'] ?? '';
$headers = [
'Title: [Security] ' . $title,
'Priority: 5',
'Tags: warning,shield',
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
}
catch (\Throwable $e)
{
Log::add('Ntfy security push error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
// ==================================================================
// Security Event Notifications (#131)
// ==================================================================
/**
* Send a security alert to admin emails.
*/
public static function securityAlert(string $event, string $subject, string $body): void
{
try
{
$config = self::getNotificationConfig();
$enabled = $config['security_alerts'] ?? '1';
if (!$enabled)
{
return;
}
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
$recipients = $adminEmails;
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$recipients[] = $email;
}
}
$recipients = array_unique($recipients);
if (empty($recipients))
{
return;
}
$siteName = Factory::getConfig()->get('sitename', 'Site');
$fullSubject = '[' . $siteName . ' Security] ' . $subject;
$lines = [
$siteName . ' Security Alert',
str_repeat('-', 40),
'',
'Event: ' . $event,
'Time: ' . gmdate('Y-m-d H:i:s') . ' UTC',
'',
$body,
'',
'-- ',
$siteName . ' | MokoSuiteClient Security',
];
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($fullSubject);
$mailer->setBody(implode("\n", $lines));
foreach ($recipients as $email)
{
try
{
$mailer->clearAddresses();
$mailer->addRecipient(trim($email));
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
// Also push via ntfy
self::pushNtfySecurity($event, $subject, $body);
}
catch (\Throwable $e)
{
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
}
@@ -1,33 +0,0 @@
<?php
namespace Moko\Component\MokoSuiteClient\Administrator\View\Canned;
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 $responses = [];
protected $categories = [];
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokosuiteclient_ticket_canned ORDER BY ordering ASC');
$this->responses = $db->loadObjectList() ?: [];
$db->setQuery('SELECT id, title FROM #__mokosuiteclient_ticket_categories WHERE published = 1 ORDER BY ordering');
$this->categories = $db->loadObjectList() ?: [];
ToolbarHelper::title('Canned Responses', 'comment');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
}
@@ -56,7 +56,7 @@ class HtmlView extends BaseHtmlView
} }
} }
catch (\Throwable $e) {} catch (\Throwable $e) {}
$this->recentLogins = $model->getRecentLogins(5); $this->recentLogins = $model->getRecentLogins(10);
$this->pendingUpdates = $model->getPendingUpdates(); $this->pendingUpdates = $model->getPendingUpdates();
$this->checkedOutItems = $model->getCheckedOutItems(); $this->checkedOutItems = $model->getCheckedOutItems();
$this->wafBlocks = $model->getRecentWafBlocks(5); $this->wafBlocks = $model->getRecentWafBlocks(5);
@@ -1,72 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Ticket;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $ticket;
protected $cannedResponses = [];
protected $statuses = [];
protected $priorities = [];
protected $customFields = [];
protected $fieldValues = [];
protected $attachments = [];
public function display($tpl = null)
{
$model = $this->getModel('Tickets');
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->ticket = $model->getTicket($id);
$this->cannedResponses = $model->getCannedResponses((int) ($this->ticket->category_id ?? 0));
$this->statuses = $model->getStatuses();
$this->priorities = $model->getPriorities();
// Load custom fields for this ticket's category
if ($this->ticket && $this->ticket->category_id)
{
$groups = $model->getFieldGroupsForCategory((int) $this->ticket->category_id);
$groupIds = array_column($groups, 'id');
$this->customFields = $model->getFieldsForGroups($groupIds);
$this->fieldValues = $model->getFieldValues($id);
}
// Load attachments
$this->attachments = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getForTicket($id);
if (!$this->ticket)
{
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
Factory::getApplication()->redirect('index.php?option=com_mokosuiteclient&view=tickets');
return;
}
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
$title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket';
ToolbarHelper::title($title, 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient&view=tickets');
}
}
@@ -1,67 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Tickets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $tickets = [];
protected $categories = [];
protected $statusCounts;
protected $overdue = [];
protected $atsAvailable = null;
protected $contacts = [];
protected $statuses = [];
protected $priorities = [];
protected $backendUsers = [];
protected $userGroups = [];
public function display($tpl = null)
{
$model = $this->getModel();
$app = Factory::getApplication();
$filters = [
'status_id' => $app->getInput()->getInt('filter_status', 0),
'priority_id' => $app->getInput()->getInt('filter_priority', 0),
'category_id' => $app->getInput()->getInt('filter_category', 0),
'contact_id' => $app->getInput()->getInt('filter_contact', 0),
];
$this->tickets = $model->getTickets($filters);
$this->categories = $model->getCategories();
$this->statuses = $model->getStatuses();
$this->priorities = $model->getPriorities();
$this->statusCounts = $model->getStatusCounts();
$this->overdue = $model->getOverdueTickets();
$this->atsAvailable = $model->checkAtsAvailable();
$this->contacts = $model->getContacts();
$this->backendUsers = $model->getBackendUsers();
$this->userGroups = $model->getUserGroups();
$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(Text::_('COM_MOKOSUITECLIENT_TICKETS_TITLE'), 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,41 +0,0 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* @package MokoSuiteClient
* @subpackage Component
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Ticketsettings;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $statuses = [];
protected $priorities = [];
public function display($tpl = null)
{
$model = $this->getModel('Tickets');
$this->statuses = $model->getStatuses();
$this->priorities = $model->getPriorities();
$this->addToolbar();
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOSUITECLIENT_TICKET_SETTINGS'), 'cog');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient&view=tickets');
}
}
@@ -1 +0,0 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -1,227 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$responses = $this->responses;
$categories = $this->categories;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.saveCanned&format=json');
$deleteUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.deleteCanned&format=json');
$reorderUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.reorderCanned&format=json');
// Build category map for filter display
$catMap = [0 => 'All Categories'];
foreach ($categories as $cat)
{
$catMap[$cat->id] = $cat->title;
}
?>
<div id="mokosuiteclient-canned">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h4 class="mb-0"><?php echo count($responses); ?> Canned Responses</h4>
<select id="canned-filter-category" class="form-select form-select-sm" style="width:auto;">
<option value="">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="openCannedModal(0)">
<span class="icon-plus"></span> Add Response
</button>
</div>
<div id="canned-list">
<?php foreach ($responses as $r): ?>
<div class="card mb-2 canned-card" data-id="<?php echo $r->id; ?>" data-category="<?php echo (int) $r->category_id; ?>" style="cursor:grab;">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1" style="cursor:pointer;" onclick="openCannedModal(<?php echo $r->id; ?>)">
<div class="d-flex align-items-center gap-2">
<span class="icon-menu text-muted" style="cursor:grab;" title="Drag to reorder"></span>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<?php if (!empty($r->category_id) && isset($catMap[$r->category_id])): ?>
<span class="badge bg-secondary"><?php echo htmlspecialchars($catMap[$r->category_id]); ?></span>
<?php endif; ?>
</div>
<p class="text-muted small mb-0 mt-1 ms-4"><?php echo htmlspecialchars(mb_substr(strip_tags($r->body), 0, 150)); ?></p>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-canned" data-id="<?php echo $r->id; ?>">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($responses)): ?>
<div class="alert alert-info" id="canned-empty">No canned responses yet. Click "Add Response" to create one.</div>
<?php endif; ?>
</div>
</div>
<!-- Canned Response Modal (create + edit) -->
<div class="modal fade" id="cannedModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 id="cannedModalTitle">Add Canned Response</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="canned-id" value="0">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" id="canned-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Category (optional)</label>
<select id="canned-category" class="form-select">
<option value="">No category</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Response Text</label>
<textarea id="canned-body" class="form-control" rows="8" required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-canned"><span class="icon-save"></span> Save</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var tokenKey = '<?php echo $token; ?>';
// ── Response data store (for edit modal) ────────────────────
var responseData = {};
<?php foreach ($responses as $r): ?>
responseData[<?php echo $r->id; ?>] = {
title: <?php echo json_encode($r->title); ?>,
body: <?php echo json_encode($r->body); ?>,
category_id: <?php echo json_encode($r->category_id ?? ''); ?>
};
<?php endforeach; ?>
// ── Open modal for create (id=0) or edit ────────────────────
window.openCannedModal = function(id) {
document.getElementById('canned-id').value = id;
if (id > 0 && responseData[id]) {
document.getElementById('cannedModalTitle').textContent = 'Edit Canned Response';
document.getElementById('canned-title').value = responseData[id].title;
document.getElementById('canned-body').value = responseData[id].body;
document.getElementById('canned-category').value = responseData[id].category_id || '';
} else {
document.getElementById('cannedModalTitle').textContent = 'Add Canned Response';
document.getElementById('canned-title').value = '';
document.getElementById('canned-body').value = '';
document.getElementById('canned-category').value = '';
}
new bootstrap.Modal(document.getElementById('cannedModal')).show();
};
// ── Save (create or update) ─────────────────────────────────
document.getElementById('btn-save-canned').addEventListener('click', function() {
var title = document.getElementById('canned-title').value.trim();
if (!title) { Joomla.renderMessages({error:['Title is required']}); return; }
var fd = new FormData();
fd.append('id', document.getElementById('canned-id').value);
fd.append('title', title);
fd.append('body', document.getElementById('canned-body').value);
fd.append('category_id', document.getElementById('canned-category').value);
fd.append(tokenKey, '1');
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) location.reload();
else Joomla.renderMessages({error:[d.message]});
});
});
// ── Delete ──────────────────────────────────────────────────
document.querySelectorAll('.btn-delete-canned').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
if (!confirm('Delete this canned response?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(tokenKey, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) card.remove(); else Joomla.renderMessages({error:[d.message]}); });
});
});
// ── Category filter ─────────────────────────────────────────
document.getElementById('canned-filter-category').addEventListener('change', function() {
var catId = this.value;
document.querySelectorAll('.canned-card').forEach(function(card) {
if (!catId || card.dataset.category === catId) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
});
// ── Drag-and-drop reorder ───────────────────────────────────
var list = document.getElementById('canned-list');
var dragCard = null;
list.addEventListener('dragstart', function(e) {
dragCard = e.target.closest('.canned-card');
if (dragCard) {
dragCard.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
}
});
list.addEventListener('dragend', function() {
if (dragCard) dragCard.style.opacity = '';
dragCard = null;
});
list.addEventListener('dragover', function(e) {
e.preventDefault();
var target = e.target.closest('.canned-card');
if (target && target !== dragCard) {
var rect = target.getBoundingClientRect();
var after = (e.clientY - rect.top) > rect.height / 2;
if (after) {
target.parentNode.insertBefore(dragCard, target.nextSibling);
} else {
target.parentNode.insertBefore(dragCard, target);
}
}
});
list.addEventListener('drop', function(e) {
e.preventDefault();
// Persist new order
var ids = [];
document.querySelectorAll('.canned-card').forEach(function(c) { ids.push(c.dataset.id); });
var fd = new FormData();
fd.append('order', JSON.stringify(ids));
fd.append(tokenKey, '1');
fetch('<?php echo $reorderUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}});
});
// Make cards draggable
document.querySelectorAll('.canned-card').forEach(function(card) {
card.setAttribute('draggable', 'true');
});
});
</script>
@@ -25,6 +25,9 @@ $atsAvail = $this->atsAvailable ?? null;
$checkedOut = $this->checkedOutItems; $checkedOut = $this->checkedOutItems;
$wafBlocks = $this->wafBlocks; $wafBlocks = $this->wafBlocks;
$token = Session::getFormToken(); $token = Session::getFormToken();
$user = \Joomla\CMS\Factory::getApplication()->getIdentity();
$canWafLog = $user->authorise('mokosuiteclient.security.waflog', 'com_mokosuiteclient')
|| $user->authorise('core.admin', 'com_mokosuiteclient');
// Group plugins by category // Group plugins by category
$grouped = []; $grouped = [];
@@ -34,75 +37,50 @@ foreach ($plugins as $plugin)
} }
$categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api']; $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
$actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_actionlogs');
?> ?>
<div id="mokosuiteclient-dashboard"> <div id="mokosuiteclient-dashboard">
<?php if (!$actionLogsEnabled): ?>
<div class="alert alert-danger d-flex align-items-center gap-2 mb-4">
<span class="icon-exclamation-triangle" style="font-size:1.25rem"></span>
<div>
<strong>Action Logs Required</strong> — MokoSuite requires Joomla's Action Logs component to be enabled for login tracking and audit compliance.
<a href="<?php echo Route::_('index.php?option=com_plugins&filter[search]=actionlog'); ?>" class="alert-link ms-1">Enable Action Log Plugins</a>
</div>
</div>
<?php endif; ?>
<!-- Site Info Bar --> <!-- Site Info Bar -->
<div class="mokosuiteclient-info-bar card mb-4"> <div class="card mb-4">
<div class="card-body d-flex flex-wrap align-items-center gap-4"> <div class="card-body d-flex flex-wrap align-items-center gap-2" style="padding:0.75rem 1.25rem;font-size:0.85rem;">
<div class="mokosuiteclient-info-item"> <span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
<span class="mokosuiteclient-info-label"><?php echo Text::_('COM_MOKOSUITECLIENT_SITE'); ?></span> <span class="fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
<span class="mokosuiteclient-info-value fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span> <span class="badge bg-primary">MokoSuite <?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">MokoSuiteClient</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span></span>
</div>
<?php if (!empty($this->supportPin)): ?> <?php if (!empty($this->supportPin)): ?>
<div class="mokosuiteclient-info-item"> <span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
<span class="mokosuiteclient-info-label">Support PIN</span> <button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" id="mokosuiteclient-btn-heartbeat-pin"
<span class="mokosuiteclient-info-value"><span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC. Ask your provider for this code to verify identity."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span></span> data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.sendHeartbeat&format=json'); ?>"
</div> data-token="<?php echo $token; ?>"
title="Send heartbeat with PIN to MokoSuiteHQ">
<span class="icon-upload" aria-hidden="true"></span>
</button>
<?php endif; ?> <?php endif; ?>
<div class="mokosuiteclient-info-item"> <span class="badge bg-secondary">Joomla <?php echo $this->escape($siteInfo->joomla_version); ?></span>
<span class="mokosuiteclient-info-label">Joomla</span> <span class="badge bg-secondary">PHP <?php echo $this->escape($siteInfo->php_version); ?></span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span> <span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">PHP</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->php_version); ?></span></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label"><?php echo Text::_('COM_MOKOSUITECLIENT_DATABASE'); ?></span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span></span>
</div>
<?php if ($siteInfo->debug): ?> <?php if ($siteInfo->debug): ?>
<span class="badge bg-warning text-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_DEBUG_ON'); ?></span> <span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?> <?php endif; ?>
<?php if ($siteInfo->offline): ?> <?php if ($siteInfo->offline): ?>
<span class="badge bg-danger"><?php echo Text::_('COM_MOKOSUITECLIENT_OFFLINE'); ?></span> <span class="badge bg-danger">Offline</span>
<?php endif; ?> <?php endif; ?>
<div class="mokosuiteclient-info-item ms-auto"> <span class="ms-auto d-flex align-items-center gap-2">
<span class="icon-globe" aria-hidden="true"></span> <span class="icon-globe" aria-hidden="true"></span>
<code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code> <code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code>
</div> </span>
</div> </div>
</div> </div>
<?php if (!empty($mokoExts)): ?>
<!-- Moko Component & Module Versions -->
<div class="d-flex flex-wrap gap-2 mb-4">
<?php
$extIcons = [
'com_mokosuiteclient' => 'icon-cogs',
'mod_mokosuiteclient_cpanel' => 'icon-tachometer-alt',
'mod_mokosuiteclient_menu' => 'icon-bars',
'mod_mokosuiteclient_cache' => 'icon-bolt',
'mod_mokosuiteclient_categories' => 'icon-folder',
];
foreach ($mokoExts as $ext):
$icon = $extIcons[$ext->element] ?? 'icon-puzzle-piece';
$label = str_replace(['mod_mokosuiteclient_', 'com_mokosuiteclient'], ['', 'Component'], $ext->element);
$label = ucfirst($label ?: 'Component');
?>
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded border bg-white" style="font-size:0.85rem;">
<span class="<?php echo $icon; ?>" aria-hidden="true" style="color:#1a2744;"></span>
<span><?php echo $this->escape($label); ?></span>
<span class="badge bg-light text-dark"><?php echo $this->escape($ext->version); ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($adminToolsAvail || $atsAvail): ?> <?php if ($adminToolsAvail || $atsAvail): ?>
<!-- Akeeba Import Banner --> <!-- Akeeba Import Banner -->
@@ -203,7 +181,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
</h3> </h3>
<div class="mokosuiteclient-plugin-grid row g-3 mb-4"> <div class="mokosuiteclient-plugin-grid row g-3 mb-4">
<?php foreach ($catPlugins as $plugin): ?> <?php foreach ($catPlugins as $plugin): ?>
<div class="col-12 col-md-6"> <div class="col-12 <?php echo $catKey === 'core' ? '' : 'col-md-6 col-lg-4'; ?>">
<div class="card mokosuiteclient-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokosuiteclient-plugin-disabled'; ?>" <div class="card mokosuiteclient-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokosuiteclient-plugin-disabled'; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>"> data-extension-id="<?php echo $plugin->extension_id; ?>">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
@@ -220,11 +198,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top"> <div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?> <?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_PROTECTED'); ?></span> <span class="badge bg-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_PROTECTED'); ?></span>
<?php elseif ($plugin->configure_only): ?> <?php elseif ($plugin->extension_id): ?>
<span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOSUITECLIENT_ENABLED') : Text::_('COM_MOKOSUITECLIENT_DISABLED'); ?>
</span>
<?php else: ?>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokosuiteclient-toggle" role="switch" <input type="checkbox" class="form-check-input mokosuiteclient-toggle" role="switch"
id="toggle-<?php echo $plugin->extension_id; ?>" id="toggle-<?php echo $plugin->extension_id; ?>"
@@ -237,7 +211,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
</label> </label>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if ($plugin->type === 'plugin'): ?> <?php if ($plugin->extension_id && $plugin->type === 'plugin'): ?>
<a href="<?php echo Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $plugin->extension_id); ?>" class="btn btn-sm btn-outline-secondary"> <a href="<?php echo Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $plugin->extension_id); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOSUITECLIENT_CONFIGURE'); ?> <span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOSUITECLIENT_CONFIGURE'); ?>
</a> </a>
@@ -254,6 +228,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<!-- Right: Charts & Information (4 cols) --> <!-- Right: Charts & Information (4 cols) -->
<div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;"> <div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;">
<?php if ($canWafLog): ?>
<!-- WAF Activity Chart --> <!-- WAF Activity Chart -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
@@ -263,6 +238,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<canvas id="mokosuiteclient-chart-waf" height="140"></canvas> <canvas id="mokosuiteclient-chart-waf" height="140"></canvas>
</div> </div>
</div> </div>
<?php endif; ?>
<!-- Login Activity Chart --> <!-- Login Activity Chart -->
<div class="card mb-3"> <div class="card mb-3">
@@ -333,6 +309,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if ($canWafLog): ?>
<!-- WAF Blocks --> <!-- WAF Blocks -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
@@ -360,6 +337,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?>
<!-- Recent Logins --> <!-- Recent Logins -->
<div class="card mb-3"> <div class="card mb-3">
@@ -369,12 +347,27 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php if (!empty($recentLogins)): ?> <?php if (!empty($recentLogins)): ?>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-striped mb-0"> <table class="table table-sm table-striped mb-0">
<thead><tr><th>User</th><th>IP</th><th>Time</th></tr></thead> <thead><tr><th>User</th><th>App</th><th>IP</th><th>Time</th></tr></thead>
<tbody> <tbody>
<?php foreach ($recentLogins as $login): ?> <?php foreach ($recentLogins as $login):
$msgData = json_decode($login->message ?? '{}');
$appKey = $msgData->app ?? '';
if (stripos($appKey, 'ADMINISTRATOR') !== false) {
$appLabel = 'Admin';
$appBadge = 'bg-dark';
} elseif (stripos($appKey, 'SITE') !== false) {
$appLabel = 'Site';
$appBadge = 'bg-info text-dark';
} else {
$appLabel = 'Unknown';
$appBadge = 'bg-secondary';
}
?>
<tr> <tr>
<td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td> <td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
<td class="text-muted"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td> <td><span class="badge <?php echo $appBadge; ?>" style="font-size:0.7rem;"><?php echo $appLabel; ?></span></td>
<?php $ip = $login->ip_address ?? ''; ?>
<td class="text-muted"><?php if ($ip && $ip !== 'COM_ACTIONLOGS_DISABLED'): ?><code><?php echo $this->escape($ip); ?></code><?php else: ?><span class="text-muted fst-italic">IP logging off</span><?php endif; ?></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, Text::_('DATE_FORMAT_LC4')); ?></td> <td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, Text::_('DATE_FORMAT_LC4')); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -1,365 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$t = $this->ticket;
$canned = $this->cannedResponses;
$token = Session::getFormToken();
$attachments = $this->attachments;
$downloadUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.downloadAttachment');
$uploadUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.uploadAttachment&format=json');
$deleteAttUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.deleteAttachment&format=json');
// Group attachments by reply_id (null = ticket-level)
$attByReply = [];
foreach ($attachments as $att) {
$key = $att->reply_id ?? 0;
$attByReply[$key][] = $att;
}
$statuses = $this->statuses ?? [];
$priorities = $this->priorities ?? [];
?>
<div id="mokosuiteclient-ticket" class="row">
<!-- Left: conversation thread -->
<div class="col-12 col-xl-8">
<!-- Original ticket -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong><?php echo $this->escape($t->created_by_name); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC2')); ?></small>
</div>
<span class="badge bg-dark">Original</span>
</div>
<div class="card-body">
<?php echo nl2br($this->escape($t->body)); ?>
<?php if (!empty($attByReply[0])): ?>
<hr>
<div class="small">
<strong>Attachments:</strong>
<?php foreach ($attByReply[0] as $att): ?>
<a href="<?php echo $downloadUrl . '&id=' . $att->id; ?>" class="d-inline-block me-3">
<span class="icon-download"></span> <?php echo $this->escape($att->filename); ?>
<span class="text-muted">(<?php echo \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::formatSize($att->filesize); ?>)</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Replies -->
<?php foreach ($t->replies as $reply): ?>
<div class="card mb-3 <?php echo $reply->is_internal ? 'border-warning' : ''; ?>">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong><?php echo $this->escape($reply->user_name ?? 'System'); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, Text::_('DATE_FORMAT_LC2')); ?></small>
</div>
<?php if ($reply->is_internal): ?>
<span class="badge bg-warning text-dark">Internal Note</span>
<?php endif; ?>
</div>
<div class="card-body">
<?php echo nl2br($this->escape($reply->body)); ?>
<?php if (!empty($attByReply[$reply->id])): ?>
<hr>
<div class="small">
<strong>Attachments:</strong>
<?php foreach ($attByReply[$reply->id] as $att): ?>
<a href="<?php echo $downloadUrl . '&id=' . $att->id; ?>" class="d-inline-block me-3">
<span class="icon-download"></span> <?php echo $this->escape($att->filename); ?>
<span class="text-muted">(<?php echo \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::formatSize($att->filesize); ?>)</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<!-- Reply form -->
<div class="card mb-3">
<div class="card-header"><strong>Reply</strong></div>
<div class="card-body">
<?php if (!empty($canned)): ?>
<div class="mb-2">
<select class="form-select form-select-sm" id="canned-select">
<option value="">Insert canned response...</option>
<?php foreach ($canned as $c): ?>
<option value="<?php echo $this->escape($c->body); ?>"><?php echo $this->escape($c->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<textarea id="reply-body" class="form-control mb-2" rows="5" placeholder="Type your reply..."></textarea>
<div class="mb-2">
<input type="file" id="reply-attachments" class="form-control form-control-sm" multiple
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.zip">
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" id="btn-reply"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.addTicketReply&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>">
<span class="icon-reply"></span> Send Reply
</button>
<button type="button" class="btn btn-outline-warning" id="btn-internal"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.addTicketReply&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>" data-internal="1">
<span class="icon-eye-slash"></span> Internal Note
</button>
</div>
</div>
</div>
</div>
<!-- Right: ticket metadata -->
<div class="col-12 col-xl-4">
<div class="card mb-3">
<div class="card-header"><strong>Details</strong></div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">Status</td><td><span class="badge <?php echo $this->escape($t->status_color ?? 'bg-secondary'); ?>"><?php echo $this->escape($t->status_title ?? $t->status); ?></span></td></tr>
<tr><td class="text-muted">Priority</td><td><span class="badge <?php echo $this->escape($t->priority_color ?? 'bg-secondary'); ?>"><?php echo $this->escape($t->priority_title ?? $t->priority); ?></span></td></tr>
<tr><td class="text-muted">Category</td><td><?php echo $this->escape($t->category_title ?? '—'); ?></td></tr>
<tr><td class="text-muted">Created By</td><td><?php echo $this->escape($t->created_by_name); ?><br><small><?php echo $this->escape($t->created_by_email ?? ''); ?></small></td></tr>
<tr><td class="text-muted">Assigned To</td><td><?php
if (!empty($t->assignees)) {
foreach ($t->assignees as $a) {
$icon = $a->assignee_type === 'group' ? '<span class="icon-users"></span> ' : '<span class="icon-user"></span> ';
echo '<div>' . $icon . $this->escape($a->name) . '</div>';
}
} else {
echo '<em>Unassigned</em>';
}
?></td></tr>
<?php if ($t->contact_id): ?>
<tr><td class="text-muted">Contact</td><td>
<a href="<?php echo Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $t->contact_id); ?>">
<?php echo $this->escape($t->contact_name ?? 'Contact #' . $t->contact_id); ?>
</a>
<?php if (!empty($t->contact_email)): ?><br><small><?php echo $this->escape($t->contact_email); ?></small><?php endif; ?>
<?php if (!empty($t->contact_phone)): ?><br><small><?php echo $this->escape($t->contact_phone); ?></small><?php endif; ?>
</td></tr>
<?php endif; ?>
<tr><td class="text-muted">Created</td><td><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC2')); ?></td></tr>
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, Text::_('DATE_FORMAT_LC2')); ?></td></tr><?php endif; ?>
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, Text::_('DATE_FORMAT_LC2')); ?></td></tr><?php endif; ?>
<tr><td class="text-muted">Replies</td><td><?php echo $t->reply_count; ?></td></tr>
</table>
</div>
</div>
<!-- SLA -->
<?php if ($t->sla_response_due || $t->sla_resolution_due): ?>
<div class="card mb-3">
<div class="card-header"><strong>SLA</strong></div>
<div class="card-body">
<?php if ($t->sla_response_due): ?>
<div class="mb-2">
<small class="text-muted">Response Due</small><br>
<?php
$responseOverdue = !$t->sla_responded && strtotime($t->sla_response_due) < time();
?>
<span class="<?php echo $t->sla_responded ? 'text-success' : ($responseOverdue ? 'text-danger fw-bold' : ''); ?>">
<?php echo $t->sla_responded ? 'Responded' : HTMLHelper::_('date', $t->sla_response_due, Text::_('DATE_FORMAT_LC4')); ?>
<?php echo $responseOverdue ? ' OVERDUE' : ''; ?>
</span>
</div>
<?php endif; ?>
<?php if ($t->sla_resolution_due): ?>
<div>
<small class="text-muted">Resolution Due</small><br>
<?php
$resolutionOverdue = !!empty($t->status_is_closed) && strtotime($t->sla_resolution_due) < time();
?>
<span class="<?php echo !empty($t->status_is_closed) ? 'text-success' : ($resolutionOverdue ? 'text-danger fw-bold' : ''); ?>">
<?php echo !empty($t->status_is_closed) ? 'Met' : HTMLHelper::_('date', $t->sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?>
<?php echo $resolutionOverdue ? ' OVERDUE' : ''; ?>
</span>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Satisfaction Rating -->
<?php
$isClosed = in_array($t->status, ['resolved', 'closed'], true);
$hasRating = !empty($t->satisfaction_rating);
?>
<?php if ($hasRating): ?>
<div class="card mb-3">
<div class="card-header"><strong>Satisfaction</strong></div>
<div class="card-body text-center">
<div class="mb-1">
<?php for ($s = 1; $s <= 5; $s++): ?>
<span style="font-size:1.5rem;color:<?php echo $s <= $t->satisfaction_rating ? '#f5a623' : '#dee2e6'; ?>;">&#9733;</span>
<?php endfor; ?>
</div>
<div class="text-muted small"><?php echo $t->satisfaction_rating; ?>/5</div>
<?php if (!empty($t->satisfaction_feedback)): ?>
<p class="small mt-2 mb-0"><?php echo $this->escape($t->satisfaction_feedback); ?></p>
<?php endif; ?>
</div>
</div>
<?php elseif ($isClosed): ?>
<div class="card mb-3" id="rating-card">
<div class="card-header"><strong>Rate this Support</strong></div>
<div class="card-body text-center">
<div class="mb-2" id="star-rating">
<?php for ($s = 1; $s <= 5; $s++): ?>
<span class="star-btn" data-value="<?php echo $s; ?>" style="font-size:2rem;cursor:pointer;color:#dee2e6;">&#9733;</span>
<?php endfor; ?>
</div>
<textarea id="rating-feedback" class="form-control form-control-sm mb-2" rows="2" placeholder="Optional feedback..."></textarea>
<button type="button" class="btn btn-primary btn-sm" id="btn-rate"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.rateTicket&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>" disabled>
Submit Rating
</button>
</div>
</div>
<?php endif; ?>
<!-- Status actions -->
<div class="card mb-3">
<div class="card-header"><strong>Actions</strong></div>
<div class="card-body d-grid gap-2">
<?php foreach ($statuses as $s): ?>
<?php if ((int) $s->id !== (int) $t->status_id): ?>
<button type="button" class="btn btn-sm btn-outline-<?php echo $s->is_closed ? 'danger' : 'secondary'; ?> btn-status"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.updateTicketStatus&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-status="<?php echo $s->id; ?>" data-token="<?php echo $token; ?>">
<?php echo $this->escape($s->title); ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<!-- Custom Fields -->
<?php if (!empty($this->customFields)): ?>
<div class="card mb-3">
<div class="card-header"><strong>Custom Fields</strong></div>
<div class="card-body">
<table class="table table-sm mb-0">
<?php foreach ($this->customFields as $field): ?>
<tr>
<td class="text-muted"><?php echo $this->escape($field->title); ?></td>
<td><?php echo $this->escape($this->fieldValues[(int) $field->id] ?? '—'); ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Canned response insert
var cannedSel = document.getElementById('canned-select');
if (cannedSel) {
cannedSel.addEventListener('change', function() {
if (this.value) { document.getElementById('reply-body').value = this.value; this.selectedIndex = 0; }
});
}
// Reply buttons (with attachment upload)
document.querySelectorAll('#btn-reply, #btn-internal').forEach(function(btn) {
btn.addEventListener('click', function() {
var body = document.getElementById('reply-body').value.trim();
var fileInput = document.getElementById('reply-attachments');
if (!body && (!fileInput || !fileInput.files.length)) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('ticket_id', el.dataset.ticket);
fd.append('body', body || '(attachment)');
fd.append('is_internal', el.dataset.internal || '0');
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (!d.success) { Joomla.renderMessages({error:[d.message]}); el.disabled = false; return; }
// Upload attachments if any
if (fileInput && fileInput.files.length > 0) {
var afd = new FormData();
afd.append('ticket_id', el.dataset.ticket);
if (d.reply_id) afd.append('reply_id', d.reply_id);
for (var i = 0; i < fileInput.files.length; i++) {
afd.append('attachments[' + i + ']', fileInput.files[i]);
}
afd.append(el.dataset.token, '1');
fetch('<?php echo $uploadUrl; ?>', {method:'POST', body:afd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(){ location.reload(); });
} else {
location.reload();
}
})
.catch(function(){ el.disabled = false; });
});
});
// Status buttons
document.querySelectorAll('.btn-status').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('ticket_id', el.dataset.ticket);
fd.append('status', el.dataset.status);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if(d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); })
.finally(function(){ el.disabled = false; });
});
});
// Star rating
var selectedRating = 0;
document.querySelectorAll('.star-btn').forEach(function(star) {
star.addEventListener('mouseenter', function() {
var val = parseInt(this.dataset.value);
document.querySelectorAll('.star-btn').forEach(function(s) {
s.style.color = parseInt(s.dataset.value) <= val ? '#f5a623' : '#dee2e6';
});
});
star.addEventListener('mouseleave', function() {
document.querySelectorAll('.star-btn').forEach(function(s) {
s.style.color = parseInt(s.dataset.value) <= selectedRating ? '#f5a623' : '#dee2e6';
});
});
star.addEventListener('click', function() {
selectedRating = parseInt(this.dataset.value);
document.getElementById('btn-rate').disabled = false;
});
});
var rateBtn = document.getElementById('btn-rate');
if (rateBtn) {
rateBtn.addEventListener('click', function() {
if (!selectedRating) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('ticket_id', el.dataset.ticket);
fd.append('rating', selectedRating);
fd.append('feedback', document.getElementById('rating-feedback').value);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if(d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); })
.finally(function(){ el.disabled = false; });
});
}
});
</script>
@@ -1,317 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$tickets = $this->tickets;
$categories = $this->categories;
$statuses = $this->statuses;
$priorities = $this->priorities;
$counts = $this->statusCounts;
$overdue = $this->overdue;
$atsAvailable = $this->atsAvailable;
$token = Session::getFormToken();
?>
<div id="mokosuiteclient-tickets">
<!-- Status summary cards -->
<div class="row g-3 mb-4">
<?php foreach ($counts as $sc): ?>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo (int) $sc->cnt; ?></span><small class="text-muted"><?php echo $this->escape($sc->title); ?></small></div></div>
<?php endforeach; ?>
<?php if (\count($overdue) > 0): ?>
<div class="col"><div class="card text-center p-2 border-danger"><span class="fw-bold fs-4 text-danger"><?php echo \count($overdue); ?></span><small class="text-danger">SLA Overdue</small></div></div>
<?php endif; ?>
</div>
<!-- New ticket + filters -->
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3">
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newTicketModal">
<span class="icon-plus"></span> New Ticket
</button>
<?php if ($atsAvailable): ?>
<button type="button" class="btn btn-outline-info" id="btn-import-ats"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>"
data-tickets="<?php echo $atsAvailable->tickets; ?>"
data-posts="<?php echo $atsAvailable->posts; ?>">
<span class="icon-upload"></span> Import from Akeeba (<?php echo $atsAvailable->tickets; ?> tickets, <?php echo $atsAvailable->posts; ?> posts)
</button>
<?php endif; ?>
</div>
<form method="get" class="d-flex gap-2">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="tickets">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Statuses</option>
<?php foreach ($statuses as $s): ?>
<option value="<?php echo $s->id; ?>" <?php echo Factory::getApplication()->getInput()->getInt('filter_status') === (int) $s->id ? 'selected' : ''; ?>><?php echo $this->escape($s->title); ?></option>
<?php endforeach; ?>
</select>
<select name="filter_priority" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Priorities</option>
<?php foreach ($priorities as $p): ?>
<option value="<?php echo $p->id; ?>" <?php echo Factory::getApplication()->getInput()->getInt('filter_priority') === (int) $p->id ? 'selected' : ''; ?>><?php echo $this->escape($p->title); ?></option>
<?php endforeach; ?>
</select>
</form>
</div>
<!-- Ticket table -->
<div class="card">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Category</th>
<th>Contact</th>
<th>Created By</th>
<th>Assigned To</th>
<th>Created</th>
<th>SLA</th>
</tr>
</thead>
<tbody>
<?php if (empty($tickets)): ?>
<tr><td colspan="10" class="text-center text-muted py-4">No tickets found.</td></tr>
<?php else: ?>
<?php foreach ($tickets as $t): ?>
<?php
$slaClass = '';
$now = time();
if ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now) $slaClass = 'table-danger';
elseif ($t->sla_resolution_due && strtotime($t->sla_resolution_due) < $now && empty($t->status_is_closed)) $slaClass = 'table-danger';
elseif ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now + 3600) $slaClass = 'table-warning';
?>
<tr class="<?php echo $slaClass; ?>">
<td><a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=' . $t->id); ?>"><?php echo $this->escape(mb_substr($t->subject, 0, 60)); ?></a></td>
<td><span class="badge <?php echo $this->escape($t->status_color ?? 'bg-secondary'); ?>"><?php echo $this->escape($t->status_title ?? $t->status); ?></span></td>
<td><span class="badge <?php echo $this->escape($t->priority_color ?? 'bg-secondary'); ?>"><?php echo $this->escape($t->priority_title ?? $t->priority); ?></span></td>
<td><?php echo $this->escape($t->category_title ?? '—'); ?></td>
<td><?php echo $t->contact_name ? '<a href="' . Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $t->contact_id) . '">' . $this->escape($t->contact_name) . '</a>' : '—'; ?></td>
<td><?php echo $this->escape($t->created_by_name ?? ''); ?></td>
<td><?php
if (!empty($t->assignees)) {
$names = [];
foreach ($t->assignees as $a) {
$icon = $a->assignee_type === 'group' ? '<span class="icon-users"></span> ' : '';
$names[] = $icon . $this->escape($a->name);
}
echo implode(', ', $names);
} else {
echo '<em>Unassigned</em>';
}
?></td>
<td class="small"><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC4')); ?></td>
<td class="small">
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
<span title="Response due"><?php echo HTMLHelper::_('date', $t->sla_response_due, Text::_('DATE_FORMAT_LC4')); ?></span>
<?php elseif ($t->sla_resolution_due): ?>
<span title="Resolution due"><?php echo HTMLHelper::_('date', $t->sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?></span>
<?php else: ?>—<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- New Ticket Modal -->
<div class="modal fade" id="newTicketModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title">New Ticket</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<!-- KB Search step -->
<div id="modal-kb-step">
<label class="form-label fw-bold">What's the issue?</label>
<div class="input-group mb-3">
<input type="text" id="modal-kb-search" class="form-control" placeholder="Describe your issue to search for existing answers...">
<button type="button" class="btn btn-outline-primary" id="modal-kb-btn"><span class="icon-search"></span></button>
</div>
<div id="modal-kb-results" class="list-group mb-3 d-none"></div>
<button type="button" class="btn btn-primary" id="modal-show-form">
<span class="icon-plus"></span> Create Ticket
</button>
</div>
<!-- Ticket form step (hidden initially) -->
<form id="modal-ticket-form" class="d-none" method="post" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.createTicket&format=json'); ?>">
<div class="mb-3">
<label class="form-label">Subject</label>
<input type="text" name="subject" id="modal-subject" class="form-control" required>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Category</label>
<select name="category_id" class="form-select">
<option value="">— Select —</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo $this->escape($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Priority</label>
<select name="priority_id" class="form-select">
<?php foreach ($priorities as $p): ?>
<option value="<?php echo $p->id; ?>" <?php echo $p->is_default ? 'selected' : ''; ?>><?php echo $this->escape($p->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Contact</label>
<select name="contact_id" class="form-select">
<option value="">— None —</option>
<?php foreach ($this->contacts as $contact): ?>
<option value="<?php echo $contact->id; ?>"><?php echo $this->escape($contact->name); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Assign Users</label>
<select name="assign_users[]" class="form-select" multiple size="4">
<?php foreach ($this->backendUsers as $u): ?>
<option value="<?php echo $u->id; ?>"><?php echo $this->escape($u->name); ?></option>
<?php endforeach; ?>
</select>
<small class="text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
<div class="col-md-6">
<label class="form-label">Assign Groups</label>
<select name="assign_groups[]" class="form-select" multiple size="4">
<?php foreach ($this->userGroups as $g): ?>
<option value="<?php echo $g->id; ?>"><?php echo $this->escape($g->title); ?></option>
<?php endforeach; ?>
</select>
<small class="text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="body" class="form-control" rows="6" required></textarea>
</div>
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><span class="icon-plus"></span> Create Ticket</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// Modal KB search
var modalSearch = document.getElementById('modal-kb-search');
var modalSearchBtn = document.getElementById('modal-kb-btn');
var modalResults = document.getElementById('modal-kb-results');
var modalShowForm = document.getElementById('modal-show-form');
var modalKbStep = document.getElementById('modal-kb-step');
var modalForm = document.getElementById('modal-ticket-form');
var modalSubject = document.getElementById('modal-subject');
function modalDoSearch() {
var q = modalSearch.value.trim();
if (q.length < 3) return;
fetch('<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.searchKb&format=json'); ?>&q=' + encodeURIComponent(q), {
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d) {
modalResults.textContent = '';
if (d.results && d.results.length > 0) {
d.results.forEach(function(item) {
var a = document.createElement('a');
a.href = item.url;
a.target = '_blank';
a.className = 'list-group-item list-group-item-action';
var strong = document.createElement('strong');
strong.textContent = item.title;
a.appendChild(strong);
if (item.description) {
a.appendChild(document.createElement('br'));
var small = document.createElement('small');
small.className = 'text-muted';
small.textContent = item.description;
a.appendChild(small);
}
modalResults.appendChild(a);
});
modalResults.classList.remove('d-none');
} else {
modalResults.classList.add('d-none');
}
});
}
if (modalSearchBtn) modalSearchBtn.addEventListener('click', modalDoSearch);
if (modalSearch) modalSearch.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); modalDoSearch(); } });
// Show ticket form
if (modalShowForm) {
modalShowForm.addEventListener('click', function() {
modalKbStep.classList.add('d-none');
modalForm.classList.remove('d-none');
if (modalSearch.value && !modalSubject.value) modalSubject.value = modalSearch.value;
modalSubject.focus();
});
}
// Submit ticket from modal
if (modalForm) {
modalForm.addEventListener('submit', function(e) {
e.preventDefault();
var form = this;
var fd = new FormData(form);
fetch(form.action, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { location.href = 'index.php?option=com_mokosuiteclient&view=ticket&id=' + d.id; }
else { Joomla.renderMessages({error:[d.message]}); }
});
});
}
// Reset modal on close
document.getElementById('newTicketModal').addEventListener('hidden.bs.modal', function() {
modalKbStep.classList.remove('d-none');
modalForm.classList.add('d-none');
modalResults.classList.add('d-none');
modalSearch.value = '';
modalForm.reset();
});
// ATS Import
var atsBtn = document.getElementById('btn-import-ats');
if (atsBtn) {
atsBtn.addEventListener('click', function() {
var el = this;
if (!confirm('Import ' + el.dataset.tickets + ' tickets and ' + el.dataset.posts + ' posts from Akeeba Ticket System? Duplicates will be skipped.')) return;
el.disabled = true;
el.textContent = ' Importing...';
var fd = new FormData();
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; el.textContent = 'Import Failed - Retry'; }
})
.catch(function(){ Joomla.renderMessages({error:['Network error']}); el.disabled = false; });
});
}
</script>
@@ -1,203 +0,0 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* @package MokoSuiteClient
* @subpackage Component
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$token = Session::getFormToken();
$colorOptions = [
'bg-primary', 'bg-secondary', 'bg-success', 'bg-danger',
'bg-warning text-dark', 'bg-info text-dark', 'bg-dark', 'bg-light text-dark',
];
?>
<div class="row">
<!-- Statuses -->
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="fa-solid fa-circle-dot"></span> Ticket Statuses</strong>
</div>
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead>
<tr>
<th>Title</th>
<th class="w-10 text-center">Color</th>
<th class="w-10 text-center">Default</th>
<th class="w-10 text-center">Closed?</th>
<th class="w-10 text-center">Order</th>
<th class="w-10 text-center">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->statuses as $s): ?>
<tr>
<td><?php echo $this->escape($s->title); ?> <small class="text-muted">(<?php echo $this->escape($s->alias); ?>)</small></td>
<td class="text-center"><span class="badge <?php echo $this->escape($s->color); ?>">&nbsp;&nbsp;&nbsp;</span></td>
<td class="text-center"><?php echo $s->is_default ? '<span class="badge bg-success">Yes</span>' : ''; ?></td>
<td class="text-center"><?php echo $s->is_closed ? '<span class="badge bg-dark">Closed</span>' : ''; ?></td>
<td class="text-center"><?php echo (int) $s->ordering; ?></td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary" onclick="editStatus(<?php echo htmlspecialchars(json_encode($s)); ?>)">
<span class="icon-pencil"></span>
</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.deleteStatus&id=' . $s->id . '&' . $token . '=1'); ?>"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Delete this status?')">
<span class="icon-trash"></span>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer">
<form method="post" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.saveStatus'); ?>" id="statusForm" class="row g-2 align-items-end">
<input type="hidden" name="id" id="status-id" value="0">
<div class="col-md-3">
<label class="form-label small">Title</label>
<input type="text" name="title" id="status-title" class="form-control form-control-sm" required>
</div>
<div class="col-md-2">
<label class="form-label small">Alias</label>
<input type="text" name="alias" id="status-alias" class="form-control form-control-sm">
</div>
<div class="col-md-2">
<label class="form-label small">Color</label>
<select name="color" id="status-color" class="form-select form-select-sm">
<?php foreach ($colorOptions as $c): ?>
<option value="<?php echo $c; ?>"><?php echo $c; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-1">
<label class="form-label small">Order</label>
<input type="number" name="ordering" id="status-ordering" class="form-control form-control-sm" value="0">
</div>
<div class="col-md-1 text-center">
<label class="form-label small">Default</label>
<input type="checkbox" name="is_default" id="status-default" value="1" class="form-check-input">
</div>
<div class="col-md-1 text-center">
<label class="form-label small">Closed</label>
<input type="checkbox" name="is_closed" id="status-closed" value="1" class="form-check-input">
</div>
<div class="col-md-2">
<input type="hidden" name="<?php echo $token; ?>" value="1">
<button type="submit" class="btn btn-sm btn-primary w-100" id="status-btn">Add</button>
</div>
</form>
</div>
</div>
</div>
<!-- Priorities -->
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="fa-solid fa-flag"></span> Ticket Priorities</strong>
</div>
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead>
<tr>
<th>Title</th>
<th class="w-10 text-center">Color</th>
<th class="w-10 text-center">Default</th>
<th class="w-10 text-center">Order</th>
<th class="w-10 text-center">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->priorities as $p): ?>
<tr>
<td><?php echo $this->escape($p->title); ?> <small class="text-muted">(<?php echo $this->escape($p->alias); ?>)</small></td>
<td class="text-center"><span class="badge <?php echo $this->escape($p->color); ?>">&nbsp;&nbsp;&nbsp;</span></td>
<td class="text-center"><?php echo $p->is_default ? '<span class="badge bg-success">Yes</span>' : ''; ?></td>
<td class="text-center"><?php echo (int) $p->ordering; ?></td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary" onclick="editPriority(<?php echo htmlspecialchars(json_encode($p)); ?>)">
<span class="icon-pencil"></span>
</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.deletePriority&id=' . $p->id . '&' . $token . '=1'); ?>"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Delete this priority?')">
<span class="icon-trash"></span>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer">
<form method="post" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.savePriority'); ?>" id="priorityForm" class="row g-2 align-items-end">
<input type="hidden" name="id" id="priority-id" value="0">
<div class="col-md-3">
<label class="form-label small">Title</label>
<input type="text" name="title" id="priority-title" class="form-control form-control-sm" required>
</div>
<div class="col-md-2">
<label class="form-label small">Alias</label>
<input type="text" name="alias" id="priority-alias" class="form-control form-control-sm">
</div>
<div class="col-md-2">
<label class="form-label small">Color</label>
<select name="color" id="priority-color" class="form-select form-select-sm">
<?php foreach ($colorOptions as $c): ?>
<option value="<?php echo $c; ?>"><?php echo $c; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-1">
<label class="form-label small">Order</label>
<input type="number" name="ordering" id="priority-ordering" class="form-control form-control-sm" value="0">
</div>
<div class="col-md-1 text-center">
<label class="form-label small">Default</label>
<input type="checkbox" name="is_default" id="priority-default" value="1" class="form-check-input">
</div>
<div class="col-md-3">
<input type="hidden" name="<?php echo $token; ?>" value="1">
<button type="submit" class="btn btn-sm btn-primary w-100" id="priority-btn">Add</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
function editStatus(s) {
document.getElementById('status-id').value = s.id;
document.getElementById('status-title').value = s.title;
document.getElementById('status-alias').value = s.alias;
document.getElementById('status-color').value = s.color;
document.getElementById('status-ordering').value = s.ordering;
document.getElementById('status-default').checked = !!parseInt(s.is_default);
document.getElementById('status-closed').checked = !!parseInt(s.is_closed);
document.getElementById('status-btn').textContent = 'Update';
}
function editPriority(p) {
document.getElementById('priority-id').value = p.id;
document.getElementById('priority-title').value = p.title;
document.getElementById('priority-alias').value = p.alias;
document.getElementById('priority-color').value = p.color;
document.getElementById('priority-ordering').value = p.ordering;
document.getElementById('priority-default').checked = !!parseInt(p.is_default);
document.getElementById('priority-btn').textContent = 'Update';
}
</script>
@@ -1 +0,0 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -1,313 +0,0 @@
<?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
*/
namespace Moko\Component\MokoSuiteClient\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Helpdesk Tickets REST API controller.
*
* GET /api/index.php/v1/mokosuiteclient/tickets - list tickets
* GET /api/index.php/v1/mokosuiteclient/tickets/{id} - get single ticket with replies
* POST /api/index.php/v1/mokosuiteclient/tickets - create ticket
* PATCH /api/index.php/v1/mokosuiteclient/tickets/{id} - update ticket fields
* POST /api/index.php/v1/mokosuiteclient/tickets/{id}/reply - add reply
*
* @since 02.35.00
*/
class TicketsController extends BaseController
{
/**
* GET /tickets — list tickets with optional filters.
*/
public function displayList(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$app = Factory::getApplication();
$db = Factory::getDbo();
$input = $app->getInput();
$query = $db->getQuery(true)
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->order('t.created DESC');
// Filters
$status = $input->getString('status', '');
if ($status) {
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($status));
}
$categoryId = $input->getInt('category_id', 0);
if ($categoryId) {
$query->where($db->quoteName('t.category_id') . ' = ' . $categoryId);
}
$assignedTo = $input->getInt('assigned_to', 0);
if ($assignedTo) {
$query->where($db->quoteName('t.assigned_to') . ' = ' . $assignedTo);
}
$limit = min($input->getInt('limit', 25), 100);
$offset = $input->getInt('offset', 0);
$db->setQuery($query, $offset, $limit);
$tickets = $db->loadObjectList() ?: [];
// Total count (with same filters applied)
$countQuery = clone $query;
$countQuery->clear('select')->clear('order')->select('COUNT(*)');
$db->setQuery($countQuery);
$total = (int) $db->loadResult();
$this->sendJson(200, [
'tickets' => $tickets,
'total' => $total,
'limit' => $limit,
'offset' => $offset,
]);
}
/**
* GET /tickets/{id} — single ticket with replies and attachments.
*/
public function displayItem(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$db = Factory::getDbo();
// Ticket
$db->setQuery(
$db->getQuery(true)
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->where('t.id = ' . $id)
);
$ticket = $db->loadObject();
if (!$ticket) {
$this->sendJson(404, ['error' => 'Ticket not found']);
return;
}
// Replies
$db->setQuery(
$db->getQuery(true)
->select('r.*, u.name AS user_name')
->from($db->quoteName('#__mokosuiteclient_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where('r.ticket_id = ' . $id)
->order('r.created ASC')
);
$ticket->replies = $db->loadObjectList() ?: [];
// Attachments
$ticket->attachments = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getForTicket($id);
$this->sendJson(200, $ticket);
}
/**
* POST /tickets — create a new ticket.
*/
public function create(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$subject = $input->getString('subject', '');
$body = $input->getRaw('body', '');
if (empty($subject)) {
$this->sendJson(400, ['error' => 'Subject is required']);
return;
}
$statusId = $input->getInt('status_id', 0) ?: null;
$priorityId = $input->getInt('priority_id', 0) ?: null;
$status = $input->getString('status', 'open');
$priority = $input->getString('priority', 'normal');
// Resolve status_id from alias if not provided
if (!$statusId && $status) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($status));
$statusId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
}
if (!$priorityId && $priority) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($priority));
$priorityId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
}
$ticket = (object) [
'subject' => $subject,
'body' => $body,
'status' => $status,
'status_id' => $statusId,
'priority' => $priority,
'priority_id' => $priorityId,
'category_id' => $input->getInt('category_id', 0) ?: null,
'created_by' => (int) Factory::getUser()->id,
'assigned_to' => $input->getInt('assigned_to', 0) ?: null,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
// Trigger notification
\Moko\Component\MokoSuiteClient\Administrator\Service\NotificationService::notify('ticket_created', $ticket);
$this->sendJson(201, ['id' => (int) $ticket->id, 'message' => 'Ticket created']);
}
/**
* PATCH /tickets/{id} — update ticket fields.
*/
public function update(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$id = $input->getInt('id', 0);
$db = Factory::getDbo();
// Type-safe input extraction
$fields = [];
$intFields = ['status_id', 'priority_id', 'category_id', 'assigned_to'];
$strFields = ['status', 'priority'];
foreach ($intFields as $field) {
$value = $input->getInt($field, 0);
if ($value > 0) { $fields[$field] = $value; }
}
foreach ($strFields as $field) {
$value = $input->getString($field, '');
if ($value !== '') { $fields[$field] = $value; }
}
if (empty($fields)) {
$this->sendJson(400, ['error' => 'No fields to update']);
return;
}
// Sync status/status_id if only one is provided
if (isset($fields['status']) && !isset($fields['status_id'])) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($fields['status']));
$resolved = (int) $db->setQuery($q, 0, 1)->loadResult();
if ($resolved) { $fields['status_id'] = $resolved; }
} elseif (isset($fields['status_id']) && !isset($fields['status'])) {
$q = $db->getQuery(true)->select('alias')->from('#__mokosuiteclient_ticket_statuses')
->where('id = ' . (int) $fields['status_id']);
$alias = $db->setQuery($q, 0, 1)->loadResult();
if ($alias) { $fields['status'] = $alias; }
}
if (isset($fields['priority']) && !isset($fields['priority_id'])) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($fields['priority']));
$resolved = (int) $db->setQuery($q, 0, 1)->loadResult();
if ($resolved) { $fields['priority_id'] = $resolved; }
} elseif (isset($fields['priority_id']) && !isset($fields['priority'])) {
$q = $db->getQuery(true)->select('alias')->from('#__mokosuiteclient_ticket_priorities')
->where('id = ' . (int) $fields['priority_id']);
$alias = $db->setQuery($q, 0, 1)->loadResult();
if ($alias) { $fields['priority'] = $alias; }
}
$sets = [];
foreach ($fields as $k => $v) {
$sets[] = $db->quoteName($k) . ' = ' . (is_int($v) ? $v : $db->quote($v));
}
$sets[] = 'modified = ' . $db->quote(Factory::getDate()->toSql());
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_tickets') . ' SET ' . implode(', ', $sets) . ' WHERE id = ' . $id)->execute();
if ($db->getAffectedRows() === 0) {
$this->sendJson(404, ['error' => 'Ticket not found']);
return;
}
$this->sendJson(200, ['id' => $id, 'message' => 'Ticket updated', 'updated' => array_keys($fields)]);
}
/**
* POST /tickets/{id}/reply — add a reply.
*/
public function reply(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('id', 0);
$body = $input->getRaw('body', '');
if (!$ticketId || empty($body)) {
$this->sendJson(400, ['error' => 'ticket_id and body are required']);
return;
}
$db = Factory::getDbo();
$reply = (object) [
'ticket_id' => $ticketId,
'user_id' => (int) Factory::getUser()->id,
'body' => $body,
'is_internal' => $input->getInt('is_internal', 0),
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply, 'id');
// Notify
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_tickets')->where('id = ' . $ticketId));
$ticket = $db->loadObject();
if ($ticket) {
\Moko\Component\MokoSuiteClient\Administrator\Service\NotificationService::notify('ticket_replied', $ticket, ['reply_body' => $body]);
}
$this->sendJson(201, ['reply_id' => (int) $reply->id, 'message' => 'Reply added']);
}
// ── Helpers ──────────────────────────────────────────────────
private function requireAuth(string $action, string $asset): void
{
$user = Factory::getUser();
if (!$user->authorise($action, $asset)) {
$this->sendJson(403, ['error' => 'Not authorized']);
throw new \RuntimeException('Not authorized', 403);
}
}
private function sendJson(int $code, $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$app->close();
}
}
@@ -3,28 +3,43 @@
* @package com_mokosuiteclient * @package com_mokosuiteclient
*/ */
/* Info bar */ /* Info bar — full-width with evenly distributed items */
.mokosuiteclient-info-bar .card-body { .mokosuiteclient-info-bar .card-body {
padding: 1rem 1.5rem; padding: 0.75rem 1.25rem;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
} }
.mokosuiteclient-info-item { .mokosuiteclient-info-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.35rem;
flex: 0 1 auto;
min-width: 0;
} }
.mokosuiteclient-info-label { .mokosuiteclient-info-label {
font-size: 0.8125rem; font-size: 0.75rem;
color: #6c757d; color: #6c757d;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.025em; letter-spacing: 0.04em;
white-space: nowrap;
} }
.mokosuiteclient-info-value { .mokosuiteclient-info-value {
font-size: 0.875rem; font-size: 0.85rem;
white-space: nowrap;
} }
.mokosuiteclient-info-value .badge {
font-size: 0.8rem;
padding: 0.3em 0.6em;
}
/* Plugin cards */ /* Plugin cards */
.mokosuiteclient-plugin-card { .mokosuiteclient-plugin-card {
transition: box-shadow 0.15s ease, opacity 0.15s ease; transition: box-shadow 0.15s ease, opacity 0.15s ease;
@@ -110,6 +110,40 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
} }
// Heartbeat + PIN send button
var hbBtn = document.getElementById('mokosuiteclient-btn-heartbeat-pin');
if (hbBtn) {
hbBtn.addEventListener('click', function () {
var btn = this;
var url = btn.dataset.url;
var token = btn.dataset.token;
var icon = btn.querySelector('span');
btn.disabled = true;
if (icon) icon.className = 'icon-spinner icon-spin';
var fd = new FormData();
fd.append(token, '1');
fetch(url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.success) {
Joomla.renderMessages({message: [d.message || 'Heartbeat sent to HQ.']});
} else {
Joomla.renderMessages({error: [d.message || 'Heartbeat failed.']});
}
})
.catch(function () {
Joomla.renderMessages({error: ['Network error sending heartbeat.']});
})
.finally(function () {
btn.disabled = false;
if (icon) icon.className = 'icon-upload';
});
});
}
// Akeeba import buttons // Akeeba import buttons
['btn-import-admintools', 'btn-import-ats-dash'].forEach(function(id) { ['btn-import-admintools', 'btn-import-ats-dash'].forEach(function(id) {
var btn = document.getElementById(id); var btn = document.getElementById(id);
@@ -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.46.80</version> <version>02.47.77</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>
@@ -1,84 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Site\View\Ticket;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
class HtmlView extends BaseHtmlView
{
protected $ticket;
protected $isStaff = false;
protected $canAssign = false;
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$user = Factory::getApplication()->getIdentity();
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->isStaff = $user->authorise('core.admin') || $user->authorise('mokosuiteclient.tickets', 'com_mokosuiteclient');
$this->canAssign = $user->authorise('core.admin') || $user->authorise('mokosuiteclient.tickets.assign', 'com_mokosuiteclient');
// Get ticket — staff see any, customers see only their own
$query = $db->getQuery(true)
->select([
$db->quoteName('t') . '.*',
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('u.email', 'created_by_email'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to')
->where($db->quoteName('t.id') . ' = ' . $id);
if (!$this->isStaff)
{
$query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id);
}
$db->setQuery($query);
$this->ticket = $db->loadObject();
if (!$this->ticket)
{
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
Factory::getApplication()->redirect(Route::_('index.php?option=com_mokosuiteclient&view=tickets', false));
return;
}
// Load replies — staff see internal notes, customers don't
$query = $db->getQuery(true)
->select([
$db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'),
])
->from($db->quoteName('#__mokosuiteclient_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where($db->quoteName('r.ticket_id') . ' = ' . $id);
if (!$this->isStaff)
{
$query->where($db->quoteName('r.is_internal') . ' = 0');
}
$query->order($db->quoteName('r.created') . ' ASC');
$db->setQuery($query);
$this->ticket->replies = $db->loadObjectList() ?: [];
parent::display($tpl);
}
}
@@ -1,75 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Site\View\Tickets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $tickets = [];
protected $categories = [];
protected $isStaff = false;
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$user = Factory::getApplication()->getIdentity();
$this->isStaff = $user->authorise('core.admin')
|| $user->authorise('mokosuiteclient.tickets', 'com_mokosuiteclient');
// Staff see all tickets, customers see their own
$query = $db->getQuery(true)
->select([
$db->quoteName('t.id'),
$db->quoteName('t.subject'),
$db->quoteName('t.status'),
$db->quoteName('t.priority'),
$db->quoteName('t.created'),
$db->quoteName('t.assigned_to'),
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to');
if (!$this->isStaff)
{
$query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id);
}
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
if ($filterStatus)
{
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($filterStatus));
}
$query->order($db->quoteName('t.created') . ' DESC')->setLimit(50);
$db->setQuery($query);
$this->tickets = $db->loadObjectList() ?: [];
// Categories for new ticket form
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title')])
->from($db->quoteName('#__mokosuiteclient_ticket_categories'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->categories = $db->loadObjectList() ?: [];
parent::display($tpl);
}
}
@@ -1,241 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Factory;
$t = $this->ticket;
$isStaff = $this->isStaff;
$canAssign = $this->canAssign;
$token = Session::getFormToken();
$userId = Factory::getApplication()->getIdentity()->id;
$statusLabel = [
'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Response',
'resolved' => 'Resolved', 'closed' => 'Closed',
];
$statusClass = [
'open' => 'primary', 'in_progress' => 'info', 'waiting' => 'warning',
'resolved' => 'success', 'closed' => 'secondary',
];
?>
<div class="mokosuiteclient-portal-ticket">
<div class="mb-3">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=tickets'); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-arrow-left"></span> Back to Tickets
</a>
</div>
<div class="row">
<!-- Main column: conversation -->
<div class="col-12 <?php echo $isStaff ? 'col-lg-8' : ''; ?>">
<!-- Ticket header -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h3 class="mb-1">#<?php echo $t->id; ?> — <?php echo htmlspecialchars($t->subject); ?></h3>
<small class="text-muted">
<?php echo htmlspecialchars($t->category_title ?? 'General'); ?>
&middot; <?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?>
&middot; <?php echo ucfirst($t->priority); ?>
<?php if ($isStaff): ?>
&middot; By: <?php echo htmlspecialchars($t->created_by_name); ?>
<?php endif; ?>
</small>
</div>
<span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?> fs-6">
<?php echo $statusLabel[$t->status] ?? $t->status; ?>
</span>
</div>
</div>
</div>
<!-- Original message -->
<div class="card mb-3">
<div class="card-header">
<strong><?php echo htmlspecialchars($t->created_by_name); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></small>
</div>
<div class="card-body"><?php echo nl2br(htmlspecialchars($t->body)); ?></div>
</div>
<!-- Replies -->
<?php foreach ($t->replies as $reply): ?>
<?php
$replyIsStaffUser = ((int) $reply->user_id !== (int) $t->created_by);
$isInternal = (int) $reply->is_internal;
?>
<div class="card mb-3 <?php echo $isInternal ? 'border-warning bg-warning bg-opacity-10' : ($replyIsStaffUser ? 'border-primary' : ''); ?>">
<div class="card-header d-flex justify-content-between">
<div>
<strong><?php echo htmlspecialchars($reply->user_name ?? 'Support'); ?></strong>
<?php if ($replyIsStaffUser): ?><span class="badge bg-primary ms-1">Staff</span><?php endif; ?>
<?php if ($isInternal): ?><span class="badge bg-warning text-dark ms-1">Internal Note</span><?php endif; ?>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, 'M d, Y H:i'); ?></small>
</div>
</div>
<div class="card-body"><?php echo nl2br(htmlspecialchars($reply->body)); ?></div>
</div>
<?php endforeach; ?>
<!-- Reply form -->
<?php if (!\in_array($t->status, ['closed'])): ?>
<div class="card mt-4">
<div class="card-body">
<h5>Reply</h5>
<form id="portalReply">
<textarea name="body" class="form-control mb-3" rows="5" required placeholder="Type your reply..."></textarea>
<input type="hidden" name="ticket_id" value="<?php echo $t->id; ?>">
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<span class="icon-paper-plane"></span> Send Reply
</button>
<?php if ($isStaff): ?>
<button type="button" class="btn btn-outline-warning" id="btn-internal-note">
<span class="icon-eye-slash"></span> Internal Note
</button>
<?php endif; ?>
</div>
</form>
</div>
</div>
<?php elseif ($t->status === 'closed'): ?>
<div class="alert alert-secondary mt-4">
This ticket is closed. <a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=tickets&layout=submit'); ?>">Open a new ticket</a> if you need further help.
</div>
<?php endif; ?>
</div>
<!-- Staff sidebar -->
<?php if ($isStaff): ?>
<div class="col-12 col-lg-4">
<!-- Ticket info -->
<div class="card mb-3">
<div class="card-header"><strong>Details</strong></div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted">Status</dt>
<dd class="col-7"><span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$t->status] ?? $t->status; ?></span></dd>
<dt class="col-5 text-muted">Priority</dt>
<dd class="col-7"><?php echo ucfirst($t->priority); ?></dd>
<dt class="col-5 text-muted">Category</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->category_title ?? '—'); ?></dd>
<dt class="col-5 text-muted">Submitted By</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->created_by_name); ?><br><small class="text-muted"><?php echo htmlspecialchars($t->created_by_email ?? ''); ?></small></dd>
<dt class="col-5 text-muted">Assigned To</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->assigned_to_name ?? 'Unassigned'); ?></dd>
<dt class="col-5 text-muted">Created</dt>
<dd class="col-7"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></dd>
<dt class="col-5 text-muted">Replies</dt>
<dd class="col-7"><?php echo \count($t->replies); ?></dd>
</dl>
</div>
</div>
<!-- Status actions -->
<div class="card mb-3">
<div class="card-header"><strong>Change Status</strong></div>
<div class="card-body d-grid gap-2">
<?php foreach (['open' => 'Reopen', 'in_progress' => 'In Progress', 'waiting' => 'Waiting on Customer', 'resolved' => 'Resolve', 'closed' => 'Close'] as $s => $label): ?>
<?php if ($s !== $t->status): ?>
<button type="button" class="btn btn-sm btn-outline-<?php echo $s === 'closed' ? 'danger' : ($s === 'resolved' ? 'success' : 'secondary'); ?> btn-status"
data-status="<?php echo $s; ?>">
<?php echo $label; ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php if ($canAssign): ?>
<!-- Quick assign -->
<div class="card mb-3">
<div class="card-header"><strong>Assign</strong></div>
<div class="card-body">
<button type="button" class="btn btn-sm btn-outline-primary w-100" id="btn-assign-me">
Assign to Me
</button>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
var ticketId = <?php echo $t->id; ?>;
// Reply
var replyForm = document.getElementById('portalReply');
if (replyForm) {
replyForm.addEventListener('submit', function(e) {
e.preventDefault();
sendReply(false);
});
}
// Internal note
var internalBtn = document.getElementById('btn-internal-note');
if (internalBtn) {
internalBtn.addEventListener('click', function() { sendReply(true); });
}
function sendReply(isInternal) {
var body = replyForm.querySelector('textarea[name=body]').value.trim();
if (!body) return;
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('body', body);
fd.append('is_internal', isInternal ? '1' : '0');
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokosuiteclient&task=display.submitReply&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
}
// Status buttons
document.querySelectorAll('.btn-status').forEach(function(btn) {
btn.addEventListener('click', function() {
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('status', this.dataset.status);
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokosuiteclient&task=display.updateStatus&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
});
});
// Assign to me
var assignBtn = document.getElementById('btn-assign-me');
if (assignBtn) {
assignBtn.addEventListener('click', function() {
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('assigned_to', <?php echo $userId; ?>);
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokosuiteclient&task=display.assignTicket&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
});
}
});
</script>
@@ -1,83 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$tickets = $this->tickets;
$categories = $this->categories;
$isStaff = $this->isStaff;
$token = Session::getFormToken();
$statusLabel = [
'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Response',
'resolved' => 'Resolved', 'closed' => 'Closed',
];
$statusClass = [
'open' => 'primary', 'in_progress' => 'info', 'waiting' => 'warning',
'resolved' => 'success', 'closed' => 'secondary',
];
?>
<div class="mokosuiteclient-portal">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><?php echo $isStaff ? 'All Support Tickets' : 'My Support Tickets'; ?></h2>
<div class="d-flex gap-2">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=tickets&layout=submit'); ?>" class="btn btn-primary">
<span class="icon-plus"></span> New Ticket
</a>
<?php if ($isStaff): ?>
<form method="get" class="d-inline">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="tickets">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Statuses</option>
<?php foreach ($statusLabel as $k => $v): ?>
<option value="<?php echo $k; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $k ? 'selected' : ''; ?>><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
</form>
<?php endif; ?>
</div>
</div>
<?php if (empty($tickets)): ?>
<div class="alert alert-info">
<span class="icon-info-circle"></span>
<?php echo $isStaff ? 'No tickets found.' : 'You haven\'t submitted any support tickets yet.'; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Category</th>
<?php if ($isStaff): ?><th>Submitted By</th><th>Assigned To</th><?php endif; ?>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($tickets as $t): ?>
<tr>
<td><a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=' . $t->id); ?>"><?php echo htmlspecialchars(mb_substr($t->subject, 0, 60)); ?></a></td>
<td><span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$t->status] ?? $t->status; ?></span></td>
<td><?php echo ucfirst($t->priority); ?></td>
<td><?php echo htmlspecialchars($t->category_title ?? '—'); ?></td>
<?php if ($isStaff): ?>
<td><?php echo htmlspecialchars($t->created_by_name ?? ''); ?></td>
<td><?php echo htmlspecialchars($t->assigned_to_name ?? '<em>Unassigned</em>'); ?></td>
<?php endif; ?>
<td class="text-nowrap"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
@@ -1,204 +0,0 @@
<?php
/**
* Submit a Ticket layout — search KB first, then submit form.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$categories = $this->categories;
$token = Session::getFormToken();
$searchUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.searchKb&format=json');
$submitUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.submitTicket&format=json');
$ticketUrl = Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=');
$ticketsUrl = Route::_('index.php?option=com_mokosuiteclient&view=tickets');
// Check if Smart Search has indexed content
$finderEnabled = false;
try {
$db = \Joomla\CMS\Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT COUNT(*) FROM #__finder_links WHERE published = 1');
$finderEnabled = (int) $db->loadResult() > 0;
} catch (\Throwable $e) {}
?>
<div class="mokosuiteclient-portal">
<h2>Submit a Support Request</h2>
<?php if ($finderEnabled): ?>
<!-- Step 1: Search -->
<div id="step-search" class="mb-4">
<p class="text-muted">Before submitting, let's see if we already have an answer for you.</p>
<div class="card">
<div class="card-body">
<label class="form-label fw-bold" for="kb-search">Describe your issue</label>
<div class="input-group input-group-lg">
<input type="text" id="kb-search" class="form-control" placeholder="e.g. how do I reset my password?" autofocus>
<button type="button" class="btn btn-primary" id="kb-search-btn">
<span class="icon-search"></span> Search
</button>
</div>
</div>
</div>
<!-- Search results -->
<div id="kb-results" class="mt-3 d-none">
<h5>Related Articles</h5>
<div id="kb-results-list" class="list-group mb-3"></div>
<p class="text-muted">Didn't find what you need?</p>
</div>
<div class="mt-3">
<button type="button" class="btn btn-outline-primary" id="btn-show-form">
<span class="icon-plus"></span> Submit a Ticket Anyway
</button>
</div>
</div>
<?php endif; ?>
<!-- Step 2: Ticket Form -->
<div id="step-form" class="<?php echo $finderEnabled ? 'd-none' : ''; ?>">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-3">Ticket Details</h5>
<form id="submitTicketForm">
<div class="mb-3">
<label class="form-label" for="ticket-subject">Subject <span class="text-danger">*</span></label>
<input type="text" id="ticket-subject" name="subject" class="form-control" required placeholder="Brief description of your issue">
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="ticket-category">Category</label>
<select id="ticket-category" name="category_id" class="form-select">
<option value="">Select a category</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="ticket-priority">Priority</label>
<select id="ticket-priority" name="priority" class="form-select">
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="ticket-body">Description <span class="text-danger">*</span></label>
<textarea id="ticket-body" name="body" class="form-control" rows="8" required placeholder="Please describe your issue in detail."></textarea>
</div>
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<span class="icon-paper-plane"></span> Submit Ticket
</button>
<a href="<?php echo $ticketsUrl; ?>" class="btn btn-outline-secondary btn-lg">
My Tickets
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var searchInput = document.getElementById('kb-search');
var searchBtn = document.getElementById('kb-search-btn');
var resultBox = document.getElementById('kb-results');
var resultList = document.getElementById('kb-results-list');
var showFormBtn = document.getElementById('btn-show-form');
var stepSearch = document.getElementById('step-search');
var stepForm = document.getElementById('step-form');
var subjectField = document.getElementById('ticket-subject');
// Search
function doSearch() {
var q = (searchInput ? searchInput.value.trim() : '');
if (q.length < 3) return;
fetch('<?php echo $searchUrl; ?>&q=' + encodeURIComponent(q), {
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
resultList.textContent = '';
if (d.results && d.results.length > 0) {
d.results.forEach(function(item) {
var a = document.createElement('a');
a.href = item.url;
a.target = '_blank';
a.className = 'list-group-item list-group-item-action';
var strong = document.createElement('strong');
strong.textContent = item.title;
a.appendChild(strong);
if (item.description) {
a.appendChild(document.createElement('br'));
var small = document.createElement('small');
small.className = 'text-muted';
small.textContent = item.description;
a.appendChild(small);
}
resultList.appendChild(a);
});
resultBox.classList.remove('d-none');
} else {
resultBox.classList.add('d-none');
}
// Always show the "submit anyway" button after search
if (showFormBtn) showFormBtn.classList.remove('d-none');
});
}
if (searchBtn) searchBtn.addEventListener('click', doSearch);
if (searchInput) {
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); doSearch(); }
});
}
// Show form and prefill subject from search query
if (showFormBtn) {
showFormBtn.addEventListener('click', function() {
if (stepSearch) stepSearch.classList.add('d-none');
if (stepForm) stepForm.classList.remove('d-none');
if (searchInput && subjectField && !subjectField.value) {
subjectField.value = searchInput.value;
}
subjectField.focus();
});
}
// Submit ticket
var form = document.getElementById('submitTicketForm');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = form.querySelector('button[type=submit]');
btn.disabled = true;
btn.textContent = ' Submitting...';
var fd = new FormData(form);
fetch('<?php echo $submitUrl; ?>', {
method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.id) {
window.location.href = '<?php echo $ticketUrl; ?>' + d.id;
} else {
alert(d.message || 'Failed.');
btn.disabled = false;
btn.textContent = ' Submit Ticket';
}
})
.catch(function() { alert('Network error.'); btn.disabled = false; });
});
}
});
</script>
@@ -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.46.80</version> <version>02.47.77</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>
@@ -11,34 +11,25 @@ defined('_JEXEC') or die;
use Joomla\CMS\Session\Session; use Joomla\CMS\Session\Session;
$token = Session::getFormToken(); $token = Session::getFormToken();
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=clearCache&format=json'; $cacheUrl = 'index.php?option=com_mokosuiteclient&task=display.clearCache&format=json';
$tempUrl = 'index.php?option=com_mokosuiteclient&task=clearTemp&format=json'; $tempUrl = 'index.php?option=com_mokosuiteclient&task=display.clearTemp&format=json';
$domain = $domain ?? ''; $domain = $domain ?? '';
?> ?>
<style> <div class="header-item">
.mokosuiteclient-cleaner { display:flex; align-items:center; gap:0; padding:0 0.25rem; } <div class="header-item-content d-flex align-items-center gap-0" style="padding:0;">
.mokosuiteclient-cleaner-label { font-size:0.8rem; color:var(--template-text-dark,#495057); white-space:nowrap; padding-inline-end:0.35rem; } <?php if ($domain): ?>
.mokosuiteclient-cleaner-btn { cursor:pointer; padding:0.2rem 0.5rem; font-size:0.8rem; border-radius:3px; text-decoration:none; color:var(--template-text-dark,#495057); transition:background 0.15s; white-space:nowrap; } <a href="#" class="btn btn-sm btn-outline-secondary rounded-0 rounded-start border-end-0 d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-domain" title="Support key — click to copy" style="font-size:0.8rem;">
.mokosuiteclient-cleaner-btn:hover { background:rgba(0,0,0,0.08); color:var(--template-text-dark,#212529); text-decoration:none; } <span class="icon-key" aria-hidden="true"></span> <?php echo htmlspecialchars($domain); ?>
.mokosuiteclient-cleaner-sep { color:var(--template-text-dark,#adb5bd); padding:0 0.1rem; font-size:0.8rem; } </a>
.mokosuiteclient-domain { font-family:monospace; font-size:0.75rem; color:var(--template-text-dark,#6c757d); cursor:pointer; padding:0.15rem 0.4rem; border-radius:3px; transition:background 0.15s; } <?php endif; ?>
.mokosuiteclient-domain:hover { background:rgba(0,0,0,0.06); } <a href="#" class="btn btn-sm btn-outline-primary <?php echo $domain ? 'rounded-0 border-end-0' : 'rounded-0 rounded-start border-end-0'; ?> d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache" style="font-size:0.8rem;">
</style> <span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
</a>
<div class="header-item-content mokosuiteclient-cleaner"> <a href="#" class="btn btn-sm btn-outline-danger rounded-0 rounded-end d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-temp" title="Clear temp directory" style="font-size:0.8rem;">
<?php if ($domain): ?> <span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Temp
<span class="mokosuiteclient-domain" id="mokosuiteclient-domain" title="Support key — click to copy"><?php echo htmlspecialchars($domain); ?></span> </a>
<span class="mokosuiteclient-cleaner-sep">|</span> </div>
<?php endif; ?>
<span class="mokosuiteclient-cleaner-label">Clear:</span>
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache">
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
</a>
<span class="mokosuiteclient-cleaner-sep">|</span>
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-temp" title="Clear temp directory">
<span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Temp
</a>
</div> </div>
<script> <script>
@@ -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.46.80</version> <version>02.47.77</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.46.80</version> <version>02.47.77</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>
@@ -32,9 +32,11 @@ class CpanelHelper
$pkgCache = json_decode($db->loadResult() ?? '{}'); $pkgCache = json_decode($db->loadResult() ?? '{}');
return (object) [ return (object) [
'sitename' => $config->get('sitename', ''),
'mokosuiteclient_version' => $pkgCache->version ?? '', 'mokosuiteclient_version' => $pkgCache->version ?? '',
'joomla_version' => (new Version())->getShortVersion(), 'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'db_type' => $config->get('dbtype', 'mysql'),
'debug' => (bool) $config->get('debug'), 'debug' => (bool) $config->get('debug'),
'offline' => (bool) $config->get('offline'), 'offline' => (bool) $config->get('offline'),
]; ];
@@ -8,6 +8,7 @@
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session; use Joomla\CMS\Session\Session;
@@ -44,10 +45,13 @@ foreach ($plugins as $p)
} }
$labels = [ $labels = [
'mokosuiteclient' => 'Core', 'mokosuiteclient' => 'Core Engine',
'mokosuiteclient_firewall' => 'Firewall', 'mokosuiteclient_firewall' => 'Web Firewall',
'mokosuiteclient_tenant' => 'Tenant', 'mokosuiteclient_tenant' => 'Tenant Guard',
'mokosuiteclient_devtools' => 'DevTools', 'mokosuiteclient_devtools' => 'Dev Tools',
'mokosuiteclient_offline' => 'Offline Bypass',
'mokosuiteclient_dbip' => 'GeoIP Lookup',
'mokosuiteclient_license' => 'License Manager',
]; ];
$diskPct = ($disk->total_mb && $disk->total_mb > 0) $diskPct = ($disk->total_mb && $disk->total_mb > 0)
@@ -57,176 +61,30 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
?> ?>
<div class="mod-mokosuiteclient-cpanel card p-3 mb-4"> <div class="mod-mokosuiteclient-cpanel card p-3 mb-4">
<!-- Header row --> <div class="d-flex flex-wrap align-items-center gap-2" style="font-size:0.85rem;">
<div class="d-flex align-items-center gap-2"> <?php $canDashboard = Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuiteclient'); ?>
<button type="button" class="btn btn-sm btn-link p-0 text-muted" data-bs-toggle="collapse" data-bs-target="#mokosuiteclient-cpanel-body" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokosuiteclient-cpanel-body" id="mokosuiteclient-cpanel-toggle" style="font-size:1rem;line-height:1;width:1.5rem;"> <?php if ($canDashboard): ?>
<span class="fa-solid fa-caret-<?php echo $collapsed ? 'right' : 'down'; ?>" aria-hidden="true" id="mokosuiteclient-cpanel-caret"></span> <a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" style="color:#1a2744;text-decoration:none;" title="MokoSuite Dashboard"><span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem"></span></a>
</button> <?php else: ?>
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span> <span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
<strong>MokoSuiteClient</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
<?php if (!empty($supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;" title="Support PIN"><?php echo htmlspecialchars($supportPin); ?></span>
<?php endif; ?> <?php endif; ?>
<span class="fw-bold"><?php echo htmlspecialchars($siteInfo->sitename ?? ''); ?></span>
<span class="badge bg-primary">MokoSuite <?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
<?php if (!empty($supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo htmlspecialchars($supportPin); ?></span>
<?php endif; ?>
<span class="badge bg-secondary">Joomla <?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?></span>
<span class="badge bg-secondary">PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<span class="badge bg-secondary"><?php echo htmlspecialchars($siteInfo->db_type ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?> <?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span> <span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?> <?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span> <span class="badge bg-danger">Offline</span>
<?php endif; ?> <?php endif; ?>
<?php if (($counts->moko_updates ?? 0) > 0): ?> <span class="ms-auto d-flex align-items-center gap-2">
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-info text-decoration-none" title="MokoSuiteClient updates available"> <span class="icon-globe" aria-hidden="true"></span>
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->moko_updates; ?> MokoSuiteClient update<?php echo $counts->moko_updates > 1 ? 's' : ''; ?> <code><?php echo htmlspecialchars($currentIp); ?></code>
</a>
<?php endif; ?>
<?php if ($counts->updates > 0 && $counts->updates !== ($counts->moko_updates ?? 0)): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none" title="Other updates available">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates - ($counts->moko_updates ?? 0); ?> update<?php echo ($counts->updates - ($counts->moko_updates ?? 0)) > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<span class="ms-auto">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" class="btn btn-sm btn-primary">
<span class="icon-cogs" aria-hidden="true"></span>
<?php echo Text::_('MOD_MOKOSUITECLIENT_CPANEL_OPEN_DASHBOARD'); ?>
</a>
</span> </span>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var target = document.getElementById('mokosuiteclient-cpanel-body');
var caret = document.getElementById('mokosuiteclient-cpanel-caret');
if (target && caret) {
target.addEventListener('show.bs.collapse', function() { caret.className = 'fa-solid fa-caret-down'; });
target.addEventListener('hide.bs.collapse', function() { caret.className = 'fa-solid fa-caret-right'; });
}
});
</script>
<!-- Collapsible body -->
<div class="collapse<?php echo $collapsed ? '' : ' show'; ?> mt-3" id="mokosuiteclient-cpanel-body">
<?php if ($showHealth && $showStats): ?>
<!-- Health + stats row -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($healthOk): ?>
<span class="icon-check-circle text-success d-block" style="font-size:1.5rem"></span>
<small class="text-success fw-bold">Healthy</small>
<?php else: ?>
<span class="icon-exclamation-circle text-danger d-block" style="font-size:1.5rem"></span>
<small class="text-danger fw-bold">DB Error</small>
<?php endif; ?>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->articles; ?></span>
<small class="text-muted">Articles</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->users; ?></span>
<small class="text-muted">Users</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($counts->updates > 0): ?>
<span class="fw-bold d-block text-warning" style="font-size:1.25rem"><?php echo $counts->updates; ?></span>
<small class="text-warning">Updates</small>
<?php else: ?>
<span class="icon-check d-block text-success" style="font-size:1.25rem"></span>
<small class="text-muted">Up to date</small>
<?php endif; ?>
</div>
</div>
</div>
<!-- Info + plugins + actions (consolidated) -->
<div class="d-flex flex-wrap align-items-center gap-2">
<?php if ($showDisk && $diskPct !== null): ?>
<span class="text-muted d-inline-flex align-items-center gap-1">
<span class="icon-hdd" aria-hidden="true"></span>
<?php echo $diskPct; ?>%
<span class="progress d-inline-flex" style="width:40px;height:5px"><span class="progress-bar <?php echo $diskColor; ?>" style="width:<?php echo $diskPct; ?>%"></span></span>
<?php echo number_format(($disk->free_mb ?? 0) / 1024, 1); ?>G free
</span>
<?php endif; ?>
<?php if ($showIp && $currentIp): ?>
<span class="text-muted"><span class="icon-globe" aria-hidden="true"></span> <code><?php echo htmlspecialchars($currentIp); ?></code></span>
<?php endif; ?>
<?php $ssl = $ssl ?? null; if ($ssl): ?>
<span class="badge bg-<?php echo $ssl->critical ? 'danger' : ($ssl->warning ? 'warning text-dark' : 'success'); ?>" title="SSL expires <?php echo $ssl->expires; ?>">
<span class="icon-lock" aria-hidden="true"></span>
SSL <?php echo $ssl->days_remaining; ?>d
</span>
<?php endif; ?>
<?php if ($showVersions): ?>
<span class="text-muted">J<?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?> / PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<?php endif; ?>
<?php if ($showPlugins && !empty($plugins)): ?>
<span class="border-start ps-2 ms-1"></span>
<?php foreach ($plugins as $p): ?>
<?php
$label = $labels[$p->element] ?? $p->element;
$badge = $p->enabled ? 'bg-success' : 'bg-secondary';
$icon = $p->enabled ? 'icon-check' : 'icon-times';
$configUrl = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . (int) $p->extension_id);
?>
<a href="<?php echo $configUrl; ?>" class="badge <?php echo $badge; ?> text-decoration-none" title="<?php echo htmlspecialchars($p->name); ?>">
<span class="<?php echo $icon; ?>" aria-hidden="true"></span> <?php echo htmlspecialchars($label); ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($showActions): ?>
<span class="border-start ps-2 ms-1"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="mokosuiteclient-cpanel-cache"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash" aria-hidden="true"></span> Clear Cache
</button>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-refresh" aria-hidden="true"></span> Check Updates
</a>
<?php if ($counts->updates > 0): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates; ?> update<?php echo $counts->updates > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div><!-- /.collapse -->
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('mokosuiteclient-cpanel-cache');
if (!btn) return;
btn.addEventListener('click', function() {
var el = this;
var url = el.dataset.url;
var token = el.dataset.token;
el.disabled = true;
var icon = el.querySelector('span');
var origClass = icon ? icon.className : '';
if (icon) icon.className = 'icon-spinner icon-spin';
var fd = new FormData();
fd.append(token, '1');
fetch(url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) Joomla.renderMessages({message:['Cache cleared.']});
else Joomla.renderMessages({error:[d.message||'Failed']});
})
.catch(function(){Joomla.renderMessages({error:['Network error']})})
.finally(function(){
el.disabled = false;
if (icon) icon.className = origClass;
});
});
});
</script>
@@ -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.46.80</version> <version>02.47.77</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>
@@ -17,18 +17,26 @@ $app = Factory::getApplication();
$currentOption = $app->getInput()->get('option', ''); $currentOption = $app->getInput()->get('option', '');
$currentView = $app->getInput()->get('view', ''); $currentView = $app->getInput()->get('view', '');
// ── Static views for com_mokosuiteclient ────────────────────────────────── // ── Static views for com_mokosuiteclient (ACL-gated) ──────────────────────
$mokosuiteclientStaticViews = [ $user = $app->getIdentity();
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuiteclient'], $allViews = [
['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokosuiteclient&view=tickets'], ['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuiteclient', 'acl' => 'mokosuiteclient.dashboard'],
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuiteclient&view=extensions'], ['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuiteclient&view=extensions', 'acl' => 'mokosuiteclient.extensions'],
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokosuiteclient&view=htaccess'], ['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokosuiteclient&view=htaccess', 'acl' => 'mokosuiteclient.htaccess'],
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokosuiteclient&view=privacy'], ['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokosuiteclient&view=waflog', 'acl' => 'mokosuiteclient.security.waflog'],
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokosuiteclient&view=waflog'], ['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokosuiteclient&view=privacy', 'acl' => 'core.admin'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database'], ['icon' => 'fa-solid fa-code', 'title' => 'Snippets', 'link' => 'index.php?option=com_mokosuiteclient&view=snippets', 'acl' => 'mokosuiteclient.snippets.manage'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup'], ['icon' => 'fa-solid fa-file-lines', 'title' => 'Templates', 'link' => 'index.php?option=com_mokosuiteclient&view=templates', 'acl' => 'mokosuiteclient.templates.manage'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient'], ['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' => '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-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient', 'acl' => 'core.admin'],
]; ];
$isSuper = $user->authorise('core.admin', 'com_mokosuiteclient');
$mokosuiteclientStaticViews = array_filter($allViews, function ($v) use ($user, $isSuper) {
return $isSuper || $user->authorise($v['acl'], 'com_mokosuiteclient');
});
// ── Auto-discover all Moko components from #__menu ────────────────── // ── Auto-discover all Moko components from #__menu ──────────────────
$mokoComponents = []; $mokoComponents = [];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage plg_system_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.47.77
* PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles
*/
namespace Moko\Plugin\System\MokoSuiteClient\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Form field that renders a dropdown (or multi-select) of all published
* articles, grouped by category name.
*
* Usage in XML:
* <field name="related_article" type="Articles" label="Related Article" multiple="true" />
*
* @since 02.47.62
*/
class ArticlesField extends ListField
{
protected $type = 'Articles';
protected function getOptions(): array
{
$options = parent::getOptions();
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('a.id', 'value'),
$db->quoteName('a.title', 'text'),
$db->quoteName('c.title', 'category'),
])
->from($db->quoteName('#__content', 'a'))
->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid')
->where($db->quoteName('a.state') . ' = 1')
->order($db->quoteName('a.title') . ' ASC');
$db->setQuery($query);
$articles = $db->loadObjectList() ?: [];
foreach ($articles as $article) {
$label = $article->text;
if (!empty($article->category)) {
$label .= ' [' . $article->category . ']';
}
$options[] = HTMLHelper::_('select.option', $article->value, $label);
}
return $options;
}
}
@@ -8,7 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient * INGROUP: MokoSuiteClient
* VERSION: 02.46.80 * VERSION: 02.47.77
* 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.46.80</version> <version>02.47.77</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>
@@ -99,7 +99,94 @@
description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC" description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC"
filter="url" /> filter="url" />
<field name="monitor_signing_key" type="hidden" <field name="auto_clear_cache" type="radio" default="0"
label="Auto-Clear Cache on Save"
description="Automatically clear Joomla cache when articles, modules, or extensions are saved."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="protect_emails" type="radio" default="0"
label="Email Protection"
description="Obfuscate email addresses in HTML output to prevent spam bot harvesting. Uses JavaScript decloaking."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="snippets_enabled" type="radio" default="0"
label="Snippets"
description="Enable {snippet alias=&quot;name&quot;} content tags. Reusable text/HTML blocks stored in the database with variable substitution support."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="content_templates_enabled" type="radio" default="0"
label="Content Templates"
description="Enable {template alias=&quot;name&quot;} content tags. Loads structured template data from the content_templates table and renders introtext + fulltext."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="articles_anywhere_enabled" type="radio" default="0"
label="Articles Anywhere"
description="Enable {article id=&quot;42&quot;}[title]{/article} content tags. Insert article data anywhere using template placeholders for title, introtext, author, category, dates, images, and more."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_anywhere_enabled" type="radio" default="0"
label="Users Anywhere"
description="Allow {user} tags to display user information in content. Use {user id=&quot;42&quot;}[name]{/user} for specific users or {user name} for the current logged-in user."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_allow_email" type="radio" default="0"
label="Users: Show Email"
description="Allow the [email] placeholder in {user} tags to display the real email address. When disabled, emails are masked."
class="btn-group btn-group-yesno"
showon="users_anywhere_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_allow_username" type="radio" default="1"
label="Users: Show Username"
description="Allow the [username] placeholder in {user} tags to display the real username. When disabled, usernames are masked."
class="btn-group btn-group-yesno"
showon="users_anywhere_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="replacements_enabled" type="radio" default="0"
label="ReReplacer"
description="Enable backend-managed string and regex replacement rules. Published rules from the replacements table are applied to site and/or admin content."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="sourcerer_enabled" type="radio" default="0"
label="Code Embedding (Sourcerer)"
description="Allow embedding PHP, JavaScript, and CSS code in content via {source} tags. Security restricted."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="sourcerer_forbidden_functions" type="text" default="exec,system,passthru,shell_exec,popen,proc_open,dl,eval"
label="Forbidden PHP Functions"
description="Comma-separated list of PHP functions blocked in {source} tags."
showon="sourcerer_enabled:1" />
<field name="monitor_signing_key" type="hidden"
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2xZNnNzOTZpeTZOOGMKTHRxbndhbnU4eEozdDcrdDhXT3hoY0Yyclc2QmlmOVhNaEpnYkw0c055N0wwV1dTT2tkMmZxalBNcDFtOFNyNAo1VnNycjE3cFc5b0FNMmtmdFdsaTZ1NkhTVEYyN2pVVUJrT3o4MHZMRklMMGNGNkJCUkpYN2JVWkRpamdUMjc1ClREb3dXZy82Zk9GeWFEelBHUkJuYXFacTljU2lEYWoyNlpSTVZIbktQUERWTG92VzRPTDQzL2gwZ3BtN25nUGIKdWJlLzFFTDRUMHFRbm1Xc2FEOFZ6VStoRXFGSDRTVUtMaDVNeklGbUxFZzRlZ0xCbTBXcWdxbzZRQVBnZDVPYgoybXhmQndta3RLVm5hcWR6eG9KSytzaTVuZkYreGpxbWRMZThUdmEyTHNuTUxlZmsrODVoQ3hxS2x1eWRta1lXCjlvUk5qcDhiQWdNQkFBRUNnZ0VBQkZOUS9NSVZaV2gxdlZUMFh3TFBvUEkyZjI4TTBrM0gzN0t4MXBxK2t5QzYKenRyK1pBczBCaEFEWjAwNHJOUmRYaG45N0QxVXBJYVdLeUJFZkNZQUEzWmxneS9WQmdGR21sR3VuMWNvdGdXUQoyYzg0SWhLdzNzVFFqL2dJWUxOelFWMTBLUTJYd0JZVHZ1MWhjRFpLeUxCUGJTQ1F4cEhQUGdVcUNRNFljR3lFClErVmc1dHJUYk8wQ2xCZ1U5bkVnYU1RakRJZ0F3WVZPV203dUxJTW84UC9nT3FuT2tmaFhzdzl3VTJVYWxFeTEKRmRZbGhMbGJ0ZS9MZ3lkYlJ2RStjNEtqZVp0Z3ptc1RneEh2dzM5YVVmZUZTclFRT0FjcXc0alNzUjdMck9UZAp5bDhpelRrZVBrTVFMamFqR0pabWdPbitkRzhtUlpMa3FKcWdGaVpqRVFLQmdRRFV0L0xlU0h5SmhvY3VFL240CkZreEpaclJoWUVsWnc2WlZJUnQzWDlPQ1Nmaklab3I1ZkZlczhvUzZySFhKdGZYeWx4QUxOSjJjTUhKTTViVnUKbUFSUFU4cThBeVc0OE03cHAyNmtVVTMxNXc2OU1SUkhzbWgyekRabEtDeG5GM1NSQ3U4YW95d3hZc3RUZ3hkTgo2bDhLNHZsS1dsN3FYblBhWjZjb3lQSU9od0tCZ1FESENuRmRRdW5SMVI2dkxGaVFZMTRiT3QwT0tzVGJYMUJyCmpvUGZySkxvRm5mSCs4VDVnNUdxYkV5T2p0WG1tRXhmTFFpcDBQVXRtc1E0YXlJRFBZYWZtU3RpK2dtQXZFd1MKZTlKcVYxYlRuazUrYnVRZ2FlOW16REpJWkxaczRJUlhrd1Q5aDZ4Q2xKeS80TGJSRHdBU3dUVGJlY01hN3A4UgpQN0p0bjdsYnpRS0JnQzNOR2FjUTFuZktGb3N1VS9FOTQ5a2VHeEtvWjhMREpLcEp3WjgzYTlRdTF6bFhFdTlhCi9ZbklnaG1yam9VSy85VG0vOVpaMHVIUmNKcnNEdCtzTGFsaThsRC9JSDBzcEhDYzAyN2Y3cmhXc3M2N3BaRTIKY2RXNmJLL2xNWUpWQTQxRFhHNVEyZkFjUklsTHZaWFNNL3FsR21ZUEJVYlRaWUNPTnVqS000dzdBb0dBU1dBdwpLcEZnWVZxUDFVUWo0aGEvdW9vWXRBQlFVZzd4TnJWektDSVdoampDTDVkQkpqcTZtSGtVUC9tb0lUcEQ3VkpNCnYwMnBGUWJaRDNOdk5vS1gvbjRZNElRTXZNaXR3cUtqRDFEalVXQXF6N0ZScUNGbGdDQUc2V2szVnl2dG5kczEKRzhISVgwTXFCaEp4VXVDVXhsVXpoelY4RjVHZ1VsdUpDNkMyVklFQ2dZQkJWSkxpZlNVOTlHWGZtK3dPd0RWcgo2bHZoUFgxOTBGVktWQXY3aVVWTXBwWXg4Y0QxYkcyUjRLT29JbnkxYTlxdjA2ZGFzeGVQOStkVjJVMWU3MWl5CkFXWDRBVHIrYitvSGk2eUk1MXRHRk54RUxiNXZYMVpYM3VNaDlWM29iYUpuSFNjYllpKzBBNjlyRmNuNEZuLzUKWXJybWxLTzRlRHFVZkswbVFJVCtwUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2xZNnNzOTZpeTZOOGMKTHRxbndhbnU4eEozdDcrdDhXT3hoY0Yyclc2QmlmOVhNaEpnYkw0c055N0wwV1dTT2tkMmZxalBNcDFtOFNyNAo1VnNycjE3cFc5b0FNMmtmdFdsaTZ1NkhTVEYyN2pVVUJrT3o4MHZMRklMMGNGNkJCUkpYN2JVWkRpamdUMjc1ClREb3dXZy82Zk9GeWFEelBHUkJuYXFacTljU2lEYWoyNlpSTVZIbktQUERWTG92VzRPTDQzL2gwZ3BtN25nUGIKdWJlLzFFTDRUMHFRbm1Xc2FEOFZ6VStoRXFGSDRTVUtMaDVNeklGbUxFZzRlZ0xCbTBXcWdxbzZRQVBnZDVPYgoybXhmQndta3RLVm5hcWR6eG9KSytzaTVuZkYreGpxbWRMZThUdmEyTHNuTUxlZmsrODVoQ3hxS2x1eWRta1lXCjlvUk5qcDhiQWdNQkFBRUNnZ0VBQkZOUS9NSVZaV2gxdlZUMFh3TFBvUEkyZjI4TTBrM0gzN0t4MXBxK2t5QzYKenRyK1pBczBCaEFEWjAwNHJOUmRYaG45N0QxVXBJYVdLeUJFZkNZQUEzWmxneS9WQmdGR21sR3VuMWNvdGdXUQoyYzg0SWhLdzNzVFFqL2dJWUxOelFWMTBLUTJYd0JZVHZ1MWhjRFpLeUxCUGJTQ1F4cEhQUGdVcUNRNFljR3lFClErVmc1dHJUYk8wQ2xCZ1U5bkVnYU1RakRJZ0F3WVZPV203dUxJTW84UC9nT3FuT2tmaFhzdzl3VTJVYWxFeTEKRmRZbGhMbGJ0ZS9MZ3lkYlJ2RStjNEtqZVp0Z3ptc1RneEh2dzM5YVVmZUZTclFRT0FjcXc0alNzUjdMck9UZAp5bDhpelRrZVBrTVFMamFqR0pabWdPbitkRzhtUlpMa3FKcWdGaVpqRVFLQmdRRFV0L0xlU0h5SmhvY3VFL240CkZreEpaclJoWUVsWnc2WlZJUnQzWDlPQ1Nmaklab3I1ZkZlczhvUzZySFhKdGZYeWx4QUxOSjJjTUhKTTViVnUKbUFSUFU4cThBeVc0OE03cHAyNmtVVTMxNXc2OU1SUkhzbWgyekRabEtDeG5GM1NSQ3U4YW95d3hZc3RUZ3hkTgo2bDhLNHZsS1dsN3FYblBhWjZjb3lQSU9od0tCZ1FESENuRmRRdW5SMVI2dkxGaVFZMTRiT3QwT0tzVGJYMUJyCmpvUGZySkxvRm5mSCs4VDVnNUdxYkV5T2p0WG1tRXhmTFFpcDBQVXRtc1E0YXlJRFBZYWZtU3RpK2dtQXZFd1MKZTlKcVYxYlRuazUrYnVRZ2FlOW16REpJWkxaczRJUlhrd1Q5aDZ4Q2xKeS80TGJSRHdBU3dUVGJlY01hN3A4UgpQN0p0bjdsYnpRS0JnQzNOR2FjUTFuZktGb3N1VS9FOTQ5a2VHeEtvWjhMREpLcEp3WjgzYTlRdTF6bFhFdTlhCi9ZbklnaG1yam9VSy85VG0vOVpaMHVIUmNKcnNEdCtzTGFsaThsRC9JSDBzcEhDYzAyN2Y3cmhXc3M2N3BaRTIKY2RXNmJLL2xNWUpWQTQxRFhHNVEyZkFjUklsTHZaWFNNL3FsR21ZUEJVYlRaWUNPTnVqS000dzdBb0dBU1dBdwpLcEZnWVZxUDFVUWo0aGEvdW9vWXRBQlFVZzd4TnJWektDSVdoampDTDVkQkpqcTZtSGtVUC9tb0lUcEQ3VkpNCnYwMnBGUWJaRDNOdk5vS1gvbjRZNElRTXZNaXR3cUtqRDFEalVXQXF6N0ZScUNGbGdDQUc2V2szVnl2dG5kczEKRzhISVgwTXFCaEp4VXVDVXhsVXpoelY4RjVHZ1VsdUpDNkMyVklFQ2dZQkJWSkxpZlNVOTlHWGZtK3dPd0RWcgo2bHZoUFgxOTBGVktWQXY3aVVWTXBwWXg4Y0QxYkcyUjRLT29JbnkxYTlxdjA2ZGFzeGVQOStkVjJVMWU3MWl5CkFXWDRBVHIrYitvSGk2eUk1MXRHRk54RUxiNXZYMVpYM3VNaDlWM29iYUpuSFNjYllpKzBBNjlyRmNuNEZuLzUKWXJybWxLTzRlRHFVZkswbVFJVCtwUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"
filter="raw" /> filter="raw" />
</fieldset> </fieldset>
@@ -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.46.80 * VERSION: 02.47.77
* 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.46.80 * VERSION: 02.47.77
* 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.46.80</version> <version>02.47.77</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.46.80</version> <version>02.47.77</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>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field name="domain" type="text"
label="Domain"
hint="dev.clientsite.com"
filter="raw"
required="true" />
<field name="offline_bypass" type="list" default="1"
label="Offline Mode">
<option value="1">Bypass (stay online)</option>
<option value="0">Respect (go offline)</option>
</field>
<field name="robots" type="list" default="noindex, nofollow"
label="Robots">
<option value="noindex, nofollow">noindex, nofollow</option>
<option value="noindex">noindex</option>
<option value="index, follow">index, follow (production)</option>
</field>
<field name="label" type="text"
label="Label"
hint="Development, Staging, QA..."
filter="string" />
</form>
@@ -15,14 +15,10 @@ PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution." PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys" PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution." PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_LABEL="Reset Tour Prompts"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_DESC="One-shot: reset all guided tour completion flags on save. Allows tours to re-trigger for all users. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_DEVDOMAIN="Dev Domain" PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES="Mirror Domains & Staging Environments"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_DEVDOMAIN_DESC="Configure a development domain alias that bypasses offline mode and has its own robots settings." PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES_DESC="Configure domain aliases that share this site's hosting folder. Each mirror can independently bypass offline mode and control search engine indexing."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ENABLED_LABEL="Enable Dev Domain" PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_ALIASES_LABEL="Domain Mirrors"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ENABLED_DESC="Allow a development domain to bypass offline mode for testing." PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_ALIASES_DESC="Add CNAME domains for development, staging, or QA. Each mirror gets its own offline and robots settings while sharing the same database and files."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_LABEL="Dev Domain"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_DESC="The development domain alias. Leave empty to auto-detect as dev.{primary_domain}. Must point to the same hosting folder."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_BYPASS_LABEL="Bypass Offline Mode"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_BYPASS_DESC="When the main site is offline, the dev domain stays accessible for development and testing."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ROBOTS_LABEL="Robots Directive"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ROBOTS_DESC="Meta robots tag for the dev domain. Use noindex,nofollow to prevent search engines from indexing the dev site."
@@ -8,13 +8,14 @@
<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.46.80</version> <version>02.47.77</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>
<files> <files>
<folder>src</folder> <folder>src</folder>
<folder>services</folder> <folder>services</folder>
<folder>forms</folder>
<folder>language</folder> <folder>language</folder>
</files> </files>
@@ -60,43 +61,28 @@
<option value="1">JYES</option> <option value="1">JYES</option>
<option value="0">JNO</option> <option value="0">JNO</option>
</field> </field>
</fieldset>
<fieldset name="dev_domain" <field name="reset_tour_prompts" type="radio" default="0"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_DEVDOMAIN" label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_LABEL"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_DEVDOMAIN_DESC"> description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_DESC"
<field name="dev_domain_enabled" type="radio" default="1"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ENABLED_LABEL"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ENABLED_DESC"
class="btn-group btn-group-yesno"> class="btn-group btn-group-yesno">
<option value="1">JYES</option> <option value="1">JYES</option>
<option value="0">JNO</option> <option value="0">JNO</option>
</field> </field>
</fieldset>
<field name="dev_domain" type="text" default="" <fieldset name="site_aliases"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_LABEL" label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_DESC" description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES_DESC">
hint="dev.clientsite.com (auto-detected from primary domain if empty)"
showon="dev_domain_enabled:1" />
<field name="dev_domain_offline_bypass" type="radio" default="1" <field name="site_aliases" type="subform"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_BYPASS_LABEL" label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_ALIASES_LABEL"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_BYPASS_DESC" description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_ALIASES_DESC"
class="btn-group btn-group-yesno" formsource="plugins/system/mokosuiteclient_devtools/forms/site_alias_entry.xml"
showon="dev_domain_enabled:1"> multiple="true"
<option value="1">JYES</option> layout="joomla.form.field.subform.repeatable-table"
<option value="0">JNO</option> groupByFieldset="false"
</field> buttons="add,remove,move" />
<field name="dev_domain_robots" type="list" default="noindex, nofollow"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ROBOTS_LABEL"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DEVDOMAIN_ROBOTS_DESC"
showon="dev_domain_enabled:1">
<option value="noindex, nofollow">noindex, nofollow</option>
<option value="noindex">noindex</option>
<option value="index, follow">index, follow</option>
</field>
</fieldset> </fieldset>
</fields> </fields>
</config> </config>
@@ -58,21 +58,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
$config->set('offline', 1); $config->set('offline', 1);
} }
// Suppress hit recording // Suppress hit recording via com_content's native parameter
try try
{ {
$db = Factory::getDbo(); $contentParams = \Joomla\CMS\Component\ComponentHelper::getParams('com_content');
$db->setQuery( $contentParams->set('record_hits', 0);
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('hits') . ' = 0')
->where($db->quoteName('hits') . ' > 0')
)->execute();
}
catch (\Throwable $e)
{
// Silent
} }
catch (\Throwable $e) {}
} }
/** /**
@@ -126,6 +118,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
$params->set('reset_download_keys', 0); $params->set('reset_download_keys', 0);
} }
// Reset tour prompts on save if toggled on
if ($params->get('reset_tour_prompts', 0))
{
$this->resetTourPrompts();
$params->set('reset_tour_prompts', 0);
}
// Reset the one-shot toggles // Reset the one-shot toggles
if ($table->params !== $params->toString()) if ($table->params !== $params->toString())
{ {
@@ -168,6 +167,21 @@ class DevTools extends CMSPlugin implements SubscriberInterface
return $count; return $count;
} }
private function resetTourPrompts(): int
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__user_profiles'))
->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('guidedtours.tour%'))
)->execute();
$count = $db->getAffectedRows();
$this->getApplication()->enqueueMessage(\sprintf('Reset %d guided tour completion flags.', $count), 'message');
return $count;
}
private function resetDownloadKeys(): int private function resetDownloadKeys(): int
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
@@ -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.46.80</version> <version>02.47.77</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>
@@ -20,23 +20,29 @@ class CurrentIpField extends FormField
protected function getInput(): string protected function getInput(): string
{ {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''; $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$forwarded = '';
if (!empty($ip)) if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{ {
$ip = trim(explode(',', $ip)[0]); $candidate = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
if (filter_var($candidate, FILTER_VALIDATE_IP))
{
$forwarded = $candidate;
}
} }
if (empty($ip)) $html = '<div class="d-flex align-items-center gap-2">'
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
return '<div class="d-flex align-items-center gap-2">'
. '<code style="font-size:1.1rem;padding:0.4rem 0.8rem;background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;" id="mokosuiteclient-current-ip">' . '<code style="font-size:1.1rem;padding:0.4rem 0.8rem;background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;" id="mokosuiteclient-current-ip">'
. htmlspecialchars($ip) . htmlspecialchars($ip)
. '</code>' . '</code>'
. '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById(\'mokosuiteclient-current-ip\').textContent.trim()).then(function(){this.textContent=\'Copied!\';var b=this;setTimeout(function(){b.textContent=\'Copy\'},1500)}.bind(this))" title="Copy IP to clipboard">Copy</button>' . '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById(\'mokosuiteclient-current-ip\').textContent.trim()).then(function(){this.textContent=\'Copied!\';var b=this;setTimeout(function(){b.textContent=\'Copy\'},1500)}.bind(this))" title="Copy IP to clipboard">Copy</button>';
. '</div>';
if ($forwarded && $forwarded !== $ip)
{
$html .= '<span class="text-muted small ms-2">Proxy: <code>' . htmlspecialchars($forwarded) . '</code></span>';
}
return $html . '</div>';
} }
} }
@@ -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.46.80</version> <version>02.47.77</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.46.80</version> <version>02.47.77</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.46.80</version> <version>02.47.77</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>
@@ -1,8 +0,0 @@
PLG_TASK_MOKOSUITECLIENT_TICKETS="Task - MokoSuiteClient Ticket Automation"
PLG_TASK_MOKOSUITECLIENT_TICKETS_DESC="Runs scheduled helpdesk automation rules."
PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_TITLE="MokoSuiteClient: Ticket Automation"
PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_DESC="Runs time-based automation rules against open tickets (auto-close, SLA escalation, etc.)."
PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_TITLE="MokoSuiteClient: IMAP Email Polling"
PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_DESC="Polls an IMAP inbox for new emails and creates tickets or replies from unread messages."
PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_TITLE="MokoSuiteClient: Auto-Close Resolved Tickets"
PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_DESC="Automatically closes tickets that have been in resolved status longer than the configured number of days."
@@ -1,2 +0,0 @@
PLG_TASK_MOKOSUITECLIENT_TICKETS="Task - MokoSuiteClient Ticket Automation"
PLG_TASK_MOKOSUITECLIENT_TICKETS_DESC="Runs scheduled helpdesk automation rules — auto-close, SLA escalation, and time-based actions."
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteClient Ticket Automation</name>
<element>mokosuiteclient_tickets</element>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.46.80</version>
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientTickets</namespace>
<files>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_task_mokosuiteclient_tickets.ini</language>
<language tag="en-GB">en-GB/plg_task_mokosuiteclient_tickets.sys.ini</language>
</languages>
</extension>
@@ -1,27 +0,0 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\Task\MokoSuiteClientTickets\Extension\TicketAutomation;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new TicketAutomation($dispatcher, (array) PluginHelper::getPlugin('task', 'mokosuiteclient_tickets'));
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -1,367 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage plg_task_mokosuiteclient_tickets
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Plugin\Task\MokoSuiteClientTickets\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
use Joomla\Component\Scheduler\Administrator\Task\Status;
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
use Joomla\Event\SubscriberInterface;
use Moko\Component\MokoSuiteClient\Administrator\Model\TicketsModel;
use Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService;
use Moko\Component\MokoSuiteClient\Administrator\Service\NotificationService;
class TicketAutomation extends CMSPlugin implements SubscriberInterface
{
use TaskPluginTrait;
protected const TASKS_MAP = [
'mokosuiteclient.ticket.automation' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION',
'method' => 'runAutomation',
],
'mokosuiteclient.ticket.imap_poll' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL',
'method' => 'runImapPoll',
],
'mokosuiteclient.ticket.autoclose' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE',
'method' => 'runAutoClose',
],
'mokosuiteclient.license.validate' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_VALIDATE',
'method' => 'runLicenseValidation',
],
'mokosuiteclient.license.heartbeat' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_HEARTBEAT',
'method' => 'runLicenseHeartbeat',
],
];
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return [
'onTaskOptionsList' => 'advertiseRoutines',
'onExecuteTask' => 'standardRoutineHandler',
'onContentPrepareForm' => 'enhanceTaskItemForm',
];
}
/**
* Run all scheduled automation rules against open tickets.
*/
private function runAutomation(ExecuteTaskEvent $event): int
{
try
{
$model = new TicketsModel();
$results = $model->runScheduledAutomation();
$this->logTask(
\sprintf('Ticket automation: evaluated %d tickets, acted on %d', $results['evaluated'], $results['acted'])
);
return Status::OK;
}
catch (\Throwable $e)
{
$this->logTask('Ticket automation failed: ' . $e->getMessage(), 'error');
return Status::KNOCKOUT;
}
}
/**
* Poll IMAP inbox and create tickets from unread emails (#136).
*/
private function runImapPoll(ExecuteTaskEvent $event): int
{
$config = $this->getComponentConfig();
$host = $config['imap_host'] ?? '';
$port = (int) ($config['imap_port'] ?? 993);
$user = $config['imap_user'] ?? '';
$pass = $config['imap_password'] ?? '';
$ssl = ($config['imap_ssl'] ?? '1') === '1';
$folder = $config['imap_folder'] ?? 'INBOX';
$processed = $config['imap_processed_folder'] ?? 'INBOX.Processed';
$defaultCat = (int) ($config['default_category'] ?? 0) ?: null;
if (empty($host) || empty($user) || empty($pass))
{
$this->logTask('IMAP not configured — skipping', 'warning');
return Status::OK;
}
if (!function_exists('imap_open'))
{
$this->logTask('php-imap extension not available', 'error');
return Status::KNOCKOUT;
}
$mailbox = '{' . $host . ':' . $port . '/imap' . ($ssl ? '/ssl' : '') . '/novalidate-cert}' . $folder;
$mbox = @imap_open($mailbox, $user, $pass);
if (!$mbox)
{
$this->logTask('IMAP connection failed: ' . imap_last_error(), 'error');
return Status::KNOCKOUT;
}
$db = Factory::getDbo();
$created = 0;
$replied = 0;
$emails = imap_search($mbox, 'UNSEEN');
if ($emails === false)
{
imap_close($mbox);
$this->logTask('No new emails');
return Status::OK;
}
foreach ($emails as $msgNum)
{
try
{
$header = imap_headerinfo($mbox, $msgNum);
$subject = isset($header->subject) ? imap_utf8($header->subject) : '(no subject)';
$fromAddr = $header->from[0]->mailbox . '@' . $header->from[0]->host;
$body = $this->getImapBody($mbox, $msgNum);
// Match sender to Joomla user
$userId = $this->findUserByEmail($fromAddr);
// Check if this is a reply (subject contains [#123])
$ticketId = 0;
if (preg_match('/\[#(\d+)\]/', $subject, $m))
{
$ticketId = (int) $m[1];
}
if ($ticketId > 0)
{
// Add as reply to existing ticket
$reply = (object) [
'ticket_id' => $ticketId,
'user_id' => $userId,
'body' => $body,
'is_internal' => 0,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply, 'id');
$replied++;
// Notify
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_tickets')->where('id = ' . $ticketId));
$ticket = $db->loadObject();
if ($ticket) {
NotificationService::notify('ticket_replied', $ticket, ['reply_body' => $body]);
}
}
else
{
// Create new ticket
$ticket = (object) [
'subject' => $subject,
'body' => $body,
'status' => 'open',
'priority' => 'normal',
'category_id' => $defaultCat,
'created_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
$created++;
NotificationService::notify('ticket_created', $ticket);
}
// Mark as seen / move to processed folder
imap_setflag_full($mbox, (string) $msgNum, '\\Seen');
if ($processed && $processed !== $folder)
{
@imap_mail_move($mbox, (string) $msgNum, $processed);
}
}
catch (\Throwable $e)
{
Log::add('IMAP message processing error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
imap_expunge($mbox);
imap_close($mbox);
$this->logTask("IMAP poll: {$created} tickets created, {$replied} replies added");
return Status::OK;
}
/**
* Auto-close resolved tickets after configured days.
*/
private function runAutoClose(ExecuteTaskEvent $event): int
{
$config = $this->getComponentConfig();
$days = (int) ($config['autoclose_days'] ?? 7);
if ($days <= 0)
{
$this->logTask('Auto-close disabled (days = 0)');
return Status::OK;
}
$db = Factory::getDbo();
$cutoff = Factory::getDate('-' . $days . ' days')->toSql();
$db->setQuery(
"UPDATE {$db->quoteName('#__mokosuiteclient_tickets')}"
. " SET status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}"
. " WHERE status = 'resolved'"
. " AND resolved IS NOT NULL"
. " AND resolved < {$db->quote($cutoff)}"
);
$db->execute();
$closed = $db->getAffectedRows();
$this->logTask("Auto-close: {$closed} tickets closed (resolved > {$days} days ago)");
return Status::OK;
}
// ── Helpers ──────────────────────────────────────────────────
private function getComponentConfig(): array
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('params')
->from('#__extensions')
->where('element = ' . $db->quote('com_mokosuiteclient'))
->where('type = ' . $db->quote('component'))
);
return json_decode($db->loadResult() ?? '{}', true) ?: [];
}
catch (\Throwable $e)
{
Log::add('Failed to load component config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
return [];
}
}
private function findUserByEmail(string $email): int
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('id')
->from('#__users')
->where('email = ' . $db->quote($email))
->setLimit(1)
);
return (int) $db->loadResult();
}
private function getImapBody($mbox, int $msgNum): string
{
$structure = imap_fetchstructure($mbox, $msgNum);
// Simple single-part message
if (empty($structure->parts))
{
$body = imap_fetchbody($mbox, $msgNum, '1');
if ($structure->encoding === 3) $body = base64_decode($body);
if ($structure->encoding === 4) $body = quoted_printable_decode($body);
return trim(strip_tags($body));
}
// Multipart — find text/plain or text/html
$textBody = '';
foreach ($structure->parts as $i => $part)
{
$partNum = (string) ($i + 1);
if ($part->type === 0) // text
{
$content = imap_fetchbody($mbox, $msgNum, $partNum);
if ($part->encoding === 3) $content = base64_decode($content);
if ($part->encoding === 4) $content = quoted_printable_decode($content);
$subtype = strtolower($part->subtype ?? '');
if ($subtype === 'plain' && empty($textBody))
{
$textBody = $content;
}
elseif ($subtype === 'html' && empty($textBody))
{
$textBody = strip_tags($content);
}
}
}
return trim($textBody);
}
/**
* Daily license revalidation — refresh cached license status from MokoGitea.
* Recommended schedule: daily at 3:00 AM.
*/
private function runLicenseValidation(ExecuteTaskEvent $event): int
{
try {
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::validate(true);
$status = $result->valid ? 'valid' : ($result->status ?? 'invalid');
$tier = $result->tier ?? 'none';
$entitlements = count($result->entitlements ?? []);
$this->logTask(sprintf(
'License validation: status=%s tier=%s entitlements=%d',
$status, $tier, $entitlements
));
if (!$result->valid) {
Log::add('License validation failed: ' . ($result->message ?? 'unknown'), Log::WARNING, 'mokosuite.license');
}
return Status::OK;
} catch (\Throwable $e) {
$this->logTask('License validation error: ' . $e->getMessage());
return Status::KNOCKOUT;
}
}
/**
* Daily heartbeat — report active installation to MokoGitea.
* Recommended schedule: daily at 4:00 AM.
*/
private function runLicenseHeartbeat(ExecuteTaskEvent $event): int
{
try {
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::heartbeat();
$this->logTask('License heartbeat: ' . ($result->success ?? false ? 'sent' : ($result->error ?? 'failed')));
return Status::OK;
} catch (\Throwable $e) {
$this->logTask('License heartbeat error: ' . $e->getMessage());
return Status::KNOCKOUT;
}
}
}
@@ -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.46.80</version> <version>02.47.77</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.46.80 * VERSION: 02.47.77
* 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.46.80</version> <version>02.47.77</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.46.80 * VERSION: 02.47.77
* 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.46.80 * VERSION: 02.47.77
* 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.46.80</version> <version>02.47.77</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 -2
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.46.80</version> <version>02.47.77</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>
@@ -28,7 +28,6 @@
<file type="plugin" id="plg_webservices_mokosuiteclient" group="webservices">plg_webservices_mokosuiteclient.zip</file> <file type="plugin" id="plg_webservices_mokosuiteclient" group="webservices">plg_webservices_mokosuiteclient.zip</file>
<file type="plugin" id="plg_task_mokosuiteclientdemo" group="task">plg_task_mokosuiteclientdemo.zip</file> <file type="plugin" id="plg_task_mokosuiteclientdemo" group="task">plg_task_mokosuiteclientdemo.zip</file>
<file type="plugin" id="plg_task_mokosuiteclientsync" group="task">plg_task_mokosuiteclientsync.zip</file> <file type="plugin" id="plg_task_mokosuiteclientsync" group="task">plg_task_mokosuiteclientsync.zip</file>
<file type="plugin" id="plg_task_mokosuiteclient_tickets" group="task">plg_task_mokosuiteclient_tickets.zip</file>
</files> </files>
<updateservers> <updateservers>
+294 -141
View File
@@ -22,16 +22,6 @@ use Joomla\CMS\Log\Log;
*/ */
class Pkg_MokosuiteclientInstallerScript class Pkg_MokosuiteclientInstallerScript
{ {
/**
* Runs after package installation/update.
*
* @param string $type Installation type
* @param InstallerAdapter $parent Parent installer
*
* @return void
*
* @since 2.2.0
*/
/** /**
* Runs before package installation/update. * Runs before package installation/update.
* *
@@ -92,7 +82,10 @@ class Pkg_MokosuiteclientInstallerScript
$this->enablePlugin('webservices', 'mokosuiteclient'); $this->enablePlugin('webservices', 'mokosuiteclient');
$this->enablePlugin('task', 'mokosuiteclientdemo'); $this->enablePlugin('task', 'mokosuiteclientdemo');
$this->enablePlugin('task', 'mokosuiteclientsync'); $this->enablePlugin('task', 'mokosuiteclientsync');
$this->enablePlugin('task', 'mokosuiteclient_tickets');
// Ensure Joomla action log plugins are enabled (required for login tracking)
$this->enablePlugin('actionlog', 'joomla');
$this->enablePlugin('system', 'actionlogs');
// Migrate params from core plugin to feature plugins (one-time) // Migrate params from core plugin to feature plugins (one-time)
$this->migrateFeatureParams(); $this->migrateFeatureParams();
@@ -115,6 +108,9 @@ class Pkg_MokosuiteclientInstallerScript
// Set up MokoSuiteClient guided tours and unpublish Joomla defaults // Set up MokoSuiteClient guided tours and unpublish Joomla defaults
$this->setupGuidedTours(); $this->setupGuidedTours();
// Register MokoSuiteClient guided tour content (tours + steps)
$this->registerGuidedTours();
// Clean up orphaned empty-element rows and stale files from old DEFAULT '' bug // Clean up orphaned empty-element rows and stale files from old DEFAULT '' bug
$this->cleanupEmptyElements(); $this->cleanupEmptyElements();
@@ -521,47 +517,8 @@ class Pkg_MokosuiteclientInstallerScript
->where($db->quoteName('element') . ' = ' . $db->quote($element)) ->where($db->quoteName('element') . ' = ' . $db->quote($element))
); );
if ((int) $db->loadResult() > 0) // No row exists — reinstallBrokenPlugins() will handle it via
{ // Joomla's Installer which properly registers the namespace
return;
}
// No row exists — create one from the manifest XML on disk
$manifestFile = $pluginDir . '/' . $element . '.xml';
if (is_file($manifestFile))
{
$xml = @simplexml_load_file($manifestFile);
$name = $xml ? (string) ($xml->name ?? $manifestName) : $manifestName;
$namespace = $xml ? (string) ($xml->namespace ?? '') : '';
$row = (object) [
'name' => $name,
'type' => 'plugin',
'element' => $element,
'folder' => $group,
'client_id' => 0,
'enabled' => 1,
'access' => 1,
'protected' => 0,
'locked' => 0,
'params' => '{}',
'manifest_cache' => '{}',
'custom_data' => '',
'state' => 0,
'ordering' => 0,
'checked_out' => null,
'checked_out_time' => null,
];
if (!empty($namespace))
{
$row->namespace = $namespace;
}
$db->insertObject('#__extensions', $row, 'extension_id');
Log::add('Created extension record for plugin ' . $group . '/' . $element, Log::INFO, 'mokosuiteclient');
}
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
@@ -680,8 +637,8 @@ class Pkg_MokosuiteclientInstallerScript
try try
{ {
$installer = $this->installerParent->getParent(); $parentInstaller = $this->installerParent->getParent();
$sourceDir = $installer->getPath('source'); $sourceDir = $parentInstaller->getPath('source');
if (empty($sourceDir) || !is_dir($sourceDir . '/packages')) if (empty($sourceDir) || !is_dir($sourceDir . '/packages'))
{ {
@@ -691,7 +648,7 @@ class Pkg_MokosuiteclientInstallerScript
// Plugins that should exist on disk // Plugins that should exist on disk
$expected = [ $expected = [
'system' => ['mokosuiteclient_offline', 'mokosuiteclient_firewall', 'mokosuiteclient_tenant', 'mokosuiteclient_devtools', 'mokosuiteclient_dbip'], 'system' => ['mokosuiteclient_offline', 'mokosuiteclient_firewall', 'mokosuiteclient_tenant', 'mokosuiteclient_devtools', 'mokosuiteclient_dbip'],
'task' => ['mokosuiteclient_tickets'], 'task' => [],
]; ];
foreach ($expected as $group => $elements) foreach ($expected as $group => $elements)
@@ -699,13 +656,7 @@ class Pkg_MokosuiteclientInstallerScript
foreach ($elements as $element) foreach ($elements as $element)
{ {
$pluginDir = JPATH_PLUGINS . '/' . $group . '/' . $element; $pluginDir = JPATH_PLUGINS . '/' . $group . '/' . $element;
$zipName = 'plg_' . $group . '_' . $element . '.zip';
if (is_dir($pluginDir))
{
continue; // Already installed correctly
}
$zipName = 'plg_' . $group . '_' . $element . '.zip';
$zipPath = $sourceDir . '/packages/' . $zipName; $zipPath = $sourceDir . '/packages/' . $zipName;
if (!is_file($zipPath)) if (!is_file($zipPath))
@@ -713,7 +664,7 @@ class Pkg_MokosuiteclientInstallerScript
continue; continue;
} }
// Extract the zip to the correct plugin directory // Extract to plugin dir
$zip = new \ZipArchive(); $zip = new \ZipArchive();
if ($zip->open($zipPath) !== true) if ($zip->open($zipPath) !== true)
@@ -721,11 +672,101 @@ class Pkg_MokosuiteclientInstallerScript
continue; continue;
} }
if (is_dir($pluginDir))
{
$this->rmdirRecursive($pluginDir);
}
@mkdir($pluginDir, 0755, true); @mkdir($pluginDir, 0755, true);
$zip->extractTo($pluginDir); $zip->extractTo($pluginDir);
$zip->close(); $zip->close();
}
}
Log::add("Reinstalled {$group}/{$element} from package zip", Log::INFO, 'mokosuiteclient'); // Step 2: Create DB records for plugins that have files but no record
$db = Factory::getDbo();
foreach ($expected as $group => $elements)
{
foreach ($elements as $element)
{
$pluginDir = JPATH_PLUGINS . '/' . $group . '/' . $element;
$manifestFile = $pluginDir . '/' . $element . '.xml';
if (!is_file($manifestFile))
{
continue;
}
// Check if record exists
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote($group))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
);
if ((int) $db->loadResult() > 0)
{
continue;
}
// Parse manifest for name and namespace
$xml = @simplexml_load_file($manifestFile);
$name = $xml ? (string) ($xml->name ?? '') : '';
$namespace = $xml ? (string) ($xml->namespace ?? '') : '';
$version = $xml ? (string) ($xml->version ?? '') : '';
if (empty($name))
{
$name = 'plg_' . $group . '_' . $element;
}
// Build manifest cache
$cache = json_encode([
'name' => $name,
'type' => 'plugin',
'version' => $version,
'group' => $group,
]);
// INSERT with all required fields including namespace
$columns = 'name, type, element, folder, client_id, enabled, access, protected, locked, params, manifest_cache, custom_data, state, ordering';
$values = $db->quote($name) . ', '
. $db->quote('plugin') . ', '
. $db->quote($element) . ', '
. $db->quote($group) . ', '
. '0, 1, 1, 0, 0, '
. $db->quote('{}') . ', '
. $db->quote($cache) . ', '
. $db->quote('') . ', 0, 0';
$sql = "INSERT INTO " . $db->quoteName('#__extensions')
. " ({$columns}) VALUES ({$values})";
$db->setQuery($sql)->execute();
$newId = $db->insertid();
// Set namespace if column exists
if (!empty($namespace))
{
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('namespace') . ' = ' . $db->quote($namespace))
->where($db->quoteName('extension_id') . ' = ' . (int) $newId)
)->execute();
}
catch (\Throwable $e)
{
// namespace column may not exist in this Joomla version
}
}
Log::add("Created extension record for {$group}/{$element} (ID {$newId})", Log::INFO, 'mokosuiteclient');
} }
} }
} }
@@ -754,7 +795,6 @@ class Pkg_MokosuiteclientInstallerScript
$db->quote('mod_mokosuiteclient_cpanel'), $db->quote('mod_mokosuiteclient_cpanel'),
$db->quote('mokosuiteclientdemo'), $db->quote('mokosuiteclientdemo'),
$db->quote('mokosuiteclientsync'), $db->quote('mokosuiteclientsync'),
$db->quote('mokosuiteclient_tickets'),
$db->quote('mokoonyx'), $db->quote('mokoonyx'),
]; ];
@@ -1449,108 +1489,217 @@ class Pkg_MokosuiteclientInstallerScript
); );
$db->execute(); $db->execute();
// Define MokoSuiteClient tours // Remove old-format tours (superseded by com_mokosuiteclient.* UIDs)
$oldUids = [
$db->quote('mokosuiteclient-welcome'),
$db->quote('mokosuiteclient-firewall'),
$db->quote('mokosuiteclient-extensions'),
];
// Delete orphaned steps first
$subQuery = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')');
$db->setQuery($subQuery);
$oldTourIds = $db->loadColumn();
if (!empty($oldTourIds))
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__guidedtour_steps'))
->where($db->quoteName('tour_id') . ' IN (' . implode(',', array_map('intval', $oldTourIds)) . ')')
)->execute();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')')
)->execute();
}
// Tour registration is now handled by registerGuidedTours()
}
catch (\Throwable $e)
{
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Register MokoSuiteClient guided tours and their steps.
*
* Inserts tour definitions into #__guidedtours and step definitions into
* #__guidedtour_steps. Skips if the tables do not exist (pre-Joomla 4.3)
* or if a tour with the same uid already exists.
*
* @return void
*
* @since 02.47.09
*/
private function registerGuidedTours(): void
{
try
{
$db = Factory::getDbo();
// Check if #__guidedtours table exists (Joomla 4.3+)
$tables = $db->getTableList();
$prefix = $db->getPrefix();
if (!\in_array($prefix . 'guidedtours', $tables, true))
{
return;
}
$now = date('Y-m-d H:i:s');
// Define tours
$tours = [ $tours = [
[ [
'uid' => 'mokosuiteclient-welcome', 'uid' => 'com_mokosuiteclient.welcome',
'title' => 'Welcome to MokoSuiteClient', 'title' => 'MokoSuite Welcome',
'desc' => 'Get started with the MokoSuiteClient Admin Tools Suite. This tour shows you the key areas of your admin dashboard.', 'description' => 'Get started with MokoSuite — configure your health token, send your first heartbeat, and set up trusted IPs.',
'url' => 'administrator/index.php?option=com_mokosuiteclient', 'extensions' => '["com_mokosuiteclient"]',
'steps' => [ 'url' => 'administrator/index.php?option=com_mokosuiteclient',
['title' => 'MokoSuiteClient Dashboard', 'desc' => 'This is your MokoSuiteClient control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokosuiteclient-dashboard', 'type' => 0], 'steps' => [
['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokosuiteclient-info-bar', 'type' => 0], [
['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokosuiteclient-btn-cache', 'type' => 0], 'title' => 'Welcome to MokoSuite',
['title' => 'Feature Plugins', 'desc' => 'MokoSuiteClient features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokosuiteclient-plugin-grid', 'type' => 0], 'description' => 'This is your MokoSuite control panel. Let\'s walk through the key features.',
['title' => 'MokoSuiteClient Menu', 'desc' => 'The MokoSuiteClient sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokosuiteclient-admin-menu, [class*="mokosuiteclient"]', 'type' => 0], 'target' => '#mokosuiteclient-dashboard',
'type' => 2,
'position' => 'bottom',
],
[
'title' => 'Site Info Bar',
'description' => 'Your site name, version, support PIN, and system info at a glance.',
'target' => '.card.mb-4:first-child',
'type' => 2,
'position' => 'bottom',
],
[
'title' => 'Quick Actions',
'description' => 'Clear cache, check updates, manage extensions, and more.',
'target' => '#mokosuiteclient-btn-cache',
'type' => 2,
'position' => 'right',
],
[
'title' => 'Plugin Cards',
'description' => 'Enable, disable, and configure MokoSuite plugins from here.',
'target' => '.mokosuiteclient-plugin-card:first-child',
'type' => 2,
'position' => 'top',
],
], ],
], ],
[ [
'uid' => 'mokosuiteclient-firewall', 'uid' => 'com_mokosuiteclient.firewall',
'title' => 'MokoSuiteClient Firewall Setup', 'title' => 'MokoSuite Firewall Setup',
'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.', 'description' => 'Configure your Web Application Firewall — trusted IPs, WAF shields, and security headers.',
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokosuiteclient_firewall', 'extensions' => '["com_mokosuiteclient"]',
'steps' => [ 'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&extension_id=0',
['title' => 'Firewall Plugin', 'desc' => 'The MokoSuiteClient Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0], 'steps' => [
['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0], [
['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0], 'title' => 'Your Current IP',
['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0], 'description' => 'This shows your IP address. Copy it to add to the Trusted IPs list.',
], 'target' => '#mokosuiteclient-current-ip',
], 'type' => 2,
[ 'position' => 'bottom',
'uid' => 'mokosuiteclient-helpdesk', ],
'title' => 'MokoSuiteClient Helpdesk', [
'desc' => 'Learn how to manage support tickets, categories, and automation rules.', 'title' => 'Trusted IPs',
'url' => 'administrator/index.php?option=com_mokosuiteclient&view=tickets', 'description' => 'Add IPs that should bypass WAF checks — your office, VPN, etc.',
'steps' => [ 'target' => '#jform_params_trusted_ips',
['title' => 'Ticket List', 'desc' => 'View all support tickets with status, priority, SLA tracking, and assignment. Filter by status or search to find specific tickets.', 'target' => '', 'type' => 0], 'type' => 2,
['title' => 'Create a Ticket', 'desc' => 'Click the New button to create a support ticket. Assign a category, priority, and optional SLA deadline.', 'target' => '', 'type' => 0], 'position' => 'top',
['title' => 'Ticket Automation', 'desc' => 'Set up automation rules that trigger on ticket events (new ticket, status change) or Joomla events (user login, registration). Automate assignment, notifications, and status changes.', 'target' => '', 'type' => 0], ],
], [
], 'title' => 'WAF Shields',
[ 'description' => 'Enable protection against SQL injection, XSS, malicious agents, and more.',
'uid' => 'mokosuiteclient-extensions', 'target' => '#attrib-waf',
'title' => 'Moko Extensions Manager', 'type' => 2,
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.', 'position' => 'bottom',
'url' => 'administrator/index.php?option=com_mokosuiteclient&view=extensions', ],
'steps' => [
['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0],
['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0],
], ],
], ],
]; ];
foreach ($tours as $tourDef) foreach ($tours as $tourDef)
{ {
// Check if tour already exists // Check if tour already exists by uid
$db->setQuery( $query = $db->getQuery(true)
$db->getQuery(true) ->select($db->quoteName('id'))
->select('id') ->from($db->quoteName('#__guidedtours'))
->from($db->quoteName('#__guidedtours')) ->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']));
->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid'])) $db->setQuery($query);
); $existingId = (int) $db->loadResult();
if ($db->loadResult()) if ($existingId)
{ {
continue; // Update existing tour metadata
$update = $db->getQuery(true)
->update($db->quoteName('#__guidedtours'))
->set($db->quoteName('title') . ' = ' . $db->quote($tourDef['title']))
->set($db->quoteName('description') . ' = ' . $db->quote($tourDef['description']))
->set($db->quoteName('extensions') . ' = ' . $db->quote($tourDef['extensions']))
->set($db->quoteName('url') . ' = ' . $db->quote($tourDef['url']))
->set($db->quoteName('published') . ' = 1')
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($update)->execute();
// Delete existing steps so they are re-inserted fresh
$delete = $db->getQuery(true)
->delete($db->quoteName('#__guidedtour_steps'))
->where($db->quoteName('tour_id') . ' = ' . $existingId);
$db->setQuery($delete)->execute();
$tourId = $existingId;
}
else
{
// Insert new tour
$tour = (object) [
'title' => $tourDef['title'],
'uid' => $tourDef['uid'],
'description' => $tourDef['description'],
'extensions' => $tourDef['extensions'],
'url' => $tourDef['url'],
'created' => $now,
'created_by' => 0,
'modified' => $now,
'modified_by' => 0,
'published' => 1,
'language' => '*',
'note' => 'MokoSuiteClient',
'access' => 1,
'ordering' => 0,
'autostart' => 0,
];
$db->insertObject('#__guidedtours', $tour, 'id');
$tourId = (int) $tour->id;
} }
$tour = (object) [ // Insert steps
'title' => $tourDef['title'],
'uid' => $tourDef['uid'],
'description' => $tourDef['desc'],
'extensions' => '',
'url' => $tourDef['url'],
'created' => date('Y-m-d H:i:s'),
'created_by' => 0,
'modified' => date('Y-m-d H:i:s'),
'modified_by' => 0,
'published' => 1,
'language' => '*',
'note' => 'MokoSuiteClient',
'access' => 3,
'ordering' => 0,
'autostart' => 0,
];
$db->insertObject('#__guidedtours', $tour, 'id');
$tourId = (int) $tour->id;
foreach ($tourDef['steps'] as $i => $stepDef) foreach ($tourDef['steps'] as $i => $stepDef)
{ {
$step = (object) [ $step = (object) [
'tour_id' => $tourId, 'tour_id' => $tourId,
'title' => $stepDef['title'], 'title' => $stepDef['title'],
'description' => $stepDef['desc'], 'description' => $stepDef['description'],
'target' => $stepDef['target'], 'target' => $stepDef['target'],
'type' => $stepDef['type'], 'type' => $stepDef['type'],
'interactive_type' => 1, 'interactive_type' => 1,
'url' => '', 'url' => '',
'position' => 'bottom', 'position' => $stepDef['position'],
'ordering' => $i + 1, 'ordering' => $i + 1,
'published' => 1, 'published' => 1,
'created' => date('Y-m-d H:i:s'), 'created' => $now,
'created_by' => 0, 'created_by' => 0,
'modified' => date('Y-m-d H:i:s'), 'modified' => $now,
'modified_by' => 0, 'modified_by' => 0,
'language' => '*', 'language' => '*',
'note' => '', 'note' => '',
@@ -1560,10 +1709,12 @@ class Pkg_MokosuiteclientInstallerScript
$db->insertObject('#__guidedtour_steps', $step, 'id'); $db->insertObject('#__guidedtour_steps', $step, 'id');
} }
} }
Log::add('Registered ' . \count($tours) . ' MokoSuiteClient guided tours.', Log::INFO, 'mokosuiteclient');
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); Log::add('Guided tour registration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
} }
} }
@@ -1572,6 +1723,8 @@ class Pkg_MokosuiteclientInstallerScript
*/ */
private function setupSupportMenuItem(): void private function setupSupportMenuItem(): void
{ {
// Tickets moved to MokoSuiteCRM — no frontend support menu needed
return;
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();