Compare commits

..

152 Commits

Author SHA1 Message Date
gitea-actions[bot] 87ca92d9fc chore(version): pre-release bump to 02.34.79-dev [skip ci] 2026-06-10 05:44:15 +00:00
gitea-actions[bot] 71da84bc7d chore(version): pre-release bump to 02.34.78-dev [skip ci] 2026-06-10 04:32:48 +00:00
Jonathan Miller d26b980f43 fix(catalog+modules): rename MokoJoomBackup to MokoSuiteBackup, enforce module first-position ordering
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 2: Unit Tests (8.1) (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 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) 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 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) 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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 28s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 33s
Update catalog entry to point to the correct MokoSuiteBackup repo with
updated description and article URL. Add per-session ordering enforcement
so MokoSuite admin modules are always first in their position slot.
2026-06-09 23:32:21 -05:00
gitea-actions[bot] 5f07e31aaf chore(version): pre-release bump to 02.34.77-dev [skip ci] 2026-06-09 17:35:57 +00:00
Jonathan Miller ed5614886c refactor(monitor): replace raw queries and curl with Joomla APIs
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 24s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 28s
- Plugin params: use Extension Table::load/store instead of raw UPDATE
- Package version: use Extension Table::load instead of raw SELECT
- HTTP requests: use Joomla HttpFactory instead of curl_* functions
- All three methods now use Joomla's built-in abstractions
2026-06-09 12:35:39 -05:00
gitea-actions[bot] 9c2474471a chore(version): pre-release bump to 02.34.76-dev [skip ci] 2026-06-09 17:32:51 +00:00
Jonathan Miller bdbbf6d2a8 feat(monitor): send heartbeat on install/update and version change
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (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 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 31s
- onExtensionAfterInstall: immediate heartbeat after package install
- onAfterInitialise: detect version change on first admin page load,
  send heartbeat if version differs from last sent, store version
  in plugin params to avoid re-sending every session
2026-06-09 12:32:38 -05:00
gitea-actions[bot] 119a6a37b7 chore(version): pre-release bump to 02.34.75-dev [skip ci] 2026-06-09 17:24:26 +00:00
Jonathan Miller 06535d6e97 fix: enforce ordering=0 for menu module, ensure first position
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: PR Check / Validate PR (pull_request) Failing after 15s
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 34s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
2026-06-09 12:24:03 -05:00
gitea-actions[bot] cdda8fc048 chore(version): pre-release bump to 02.34.74-dev [skip ci] 2026-06-09 17:18:06 +00:00
Jonathan Miller 40e215eac4 fix: recreate admin modules if deleted, not just repair
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
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
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 52s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Failing after 11m27s
Generic: Repo Health / Access control (push) Failing after 11m28s
Self-healing now recreates module instances via ModuleModel::save()
if they were deleted from #__modules, not just if unpublished or
missing position. Checks extension is installed before attempting.
2026-06-09 12:17:52 -05:00
gitea-actions[bot] e95f294803 chore(version): pre-release bump to 02.34.73-dev [skip ci] 2026-06-09 17:16:45 +00:00
Jonathan Miller 83153ce299 feat: self-healing admin modules — check once per session
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
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 8s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 51s
System plugin checks mod_mokosuite_cpanel, menu, and cache on each
admin session. If unpublished, wrong position, or missing menu
mapping, auto-repairs via Joomla's ModuleModel::save(). Runs once
per session (flag in session storage) to avoid repeated DB hits.
2026-06-09 12:16:30 -05:00
gitea-actions[bot] 0aa22db2da chore(version): pre-release bump to 02.34.72-dev [skip ci] 2026-06-09 17:13:21 +00:00
Jonathan Miller 07960256c1 fix(cache): set line-height 2rem and fix icon alignment in status 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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 41s
Universal: PR Check / Validate PR (pull_request) Failing after 11m35s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 11m37s
2026-06-09 12:13:07 -05:00
gitea-actions[bot] 9f456adf80 chore(version): pre-release bump to 02.34.71-dev [skip ci] 2026-06-09 17:11:28 +00:00
Jonathan Miller c676f0d5d8 fix: use Joomla ModuleModel::save() for admin module setup
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 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 38s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 39s
Replace raw DB queries with Joomla's ModuleModel to properly handle
module creation, position, published state, and #__modules_menu
assignment. Fixes modules not showing after install despite being
marked as published.

Also fix cpanel default collapsed state to 0 (expanded).
2026-06-09 12:11:16 -05:00
gitea-actions[bot] 61b01e3d7a chore(version): pre-release bump to 02.34.70-dev [skip ci] 2026-06-09 16:58:08 +00:00
Jonathan Miller a7f81e533b fix: ensure modules stay published with correct positions on update
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 27s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 28s
Module setup methods now update position and published state on
reinstall/update instead of returning early when module exists.
Also add mokosuite_dbip to plugin enable list.

Modules: cpanel→top, menu→menu, cache→status
2026-06-09 11:57:52 -05:00
gitea-actions[bot] 763d2e28d5 chore(version): pre-release bump to 02.34.69-dev [skip ci] 2026-06-09 16:52:05 +00:00
Jonathan Miller ff069d7e95 fix: remove onExtensionAfterInstall hook — unsafe during package install
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 / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) 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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 40s
The system plugin is loaded before the package installer overwrites
component files, so the old code runs during install and class_exists
still fails because the autoloader has the old paths cached. Removing
the hook entirely — extension_install automation can be triggered on
next page load instead.
2026-06-09 11:51:50 -05:00
gitea-actions[bot] c57f24c664 chore(version): pre-release bump to 02.34.68-dev [skip ci] 2026-06-09 16:49:56 +00:00
Jonathan Miller fa918e9bf6 fix: guard component service calls with class_exists check
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
System plugin event handlers fire during install before com_mokosuite
is fully loaded. Guard AutomationEngine and NotificationService calls
with class_exists() to prevent fatal errors during package install.
2026-06-09 11:49:37 -05:00
gitea-actions[bot] 3b30007ea2 chore(version): pre-release bump to 02.34.67-dev [skip ci] 2026-06-09 16:45:56 +00:00
Jonathan Miller 8b9fff7282 fix: remove DEFAULT on TEXT columns for MySQL strict mode
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 50s
Universal: PR Check / Validate PR (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m1s
MySQL strict mode rejects DEFAULT values on TEXT/BLOB columns.
Remove DEFAULT '[]' from conditions and actions in automation table.
2026-06-09 11:45:37 -05:00
gitea-actions[bot] e2c15b5ca2 chore(version): pre-release bump to 02.34.66-dev [skip ci] 2026-06-09 16:21:49 +00:00
Jonathan Miller d59939a89c feat(helpdesk): IMAP email-to-ticket polling + auto-close (#136)
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 / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) 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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 21s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 40s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 13m29s
IMAP polling scheduled task:
- Connects via php-imap, reads UNSEEN messages
- Creates tickets from new emails, matches sender to Joomla user
- Detects replies via [#123] in subject line
- Marks processed emails as seen, optionally moves to folder
- IMAP config fields in component options

Auto-close scheduled task:
- Closes resolved tickets after configurable days
- Uses autoclose_days from component params

Both registered as Joomla scheduled task types in
plg_task_mokosuite_tickets alongside existing automation task.
2026-06-09 11:21:22 -05:00
gitea-actions[bot] 9a5421c0fd chore(version): pre-release bump to 02.34.65-dev [skip ci] 2026-06-09 16:17:59 +00:00
Jonathan Miller 82ea88773b docs: update changelog with v02.35.00 helpdesk features
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Has started running
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 40s
2026-06-09 11:17:42 -05:00
gitea-actions[bot] 370cab8444 chore(version): pre-release bump to 02.34.64-dev [skip ci] 2026-06-09 16:16:13 +00:00
Jonathan Miller 4349b20e34 feat(helpdesk): automation engine with Joomla event triggers (#151)
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 41s
- AutomationEngine service: fire/evaluate/execute pattern
- Condition evaluation: eq, neq, gt, lt, in, not_in operators
- Actions: set_status, set_priority, assign, add_note, send_email,
  send_ntfy, close, create_ticket
- Behavior column: append (to existing), always_new, skip_if_open
- New Joomla event triggers: user_login, user_register, content_save,
  extension_install, user_login_failed
- System plugin hooks: onUserAfterSave, onContentAfterSave,
  onExtensionAfterInstall, onUserAfterLogin (also fires automation)
- Visual builder updated with new triggers and behavior dropdown
2026-06-09 11:15:57 -05:00
gitea-actions[bot] 7234d977b8 chore(version): pre-release bump to 02.34.63-dev [skip ci] 2026-06-09 16:05:40 +00:00
Jonathan Miller 0e5c7f9396 feat(security): admin login and failed login notifications (#147)
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 33s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 35s
- onUserAfterLogin: notifies on backend admin logins with IP and username
- onUserLoginFailure: tracks failed attempts, alerts after every 3 failures
- Both use NotificationService::securityAlert (email + ntfy push)
2026-06-09 11:05:22 -05:00
gitea-actions[bot] efcdcdcfce chore(version): pre-release bump to 02.34.62-dev [skip ci] 2026-06-09 15:51:31 +00:00
Jonathan Miller b0ea119b55 feat(helpdesk): visual automation rule builder with edit + reorder (#137)
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 38s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 39s
- Visual condition builder: field/operator/value dropdowns per row
- Visual action builder: type/value dropdowns per row
- Add/remove rows dynamically, no raw JSON editing
- Click existing rule card to edit in modal
- Drag-and-drop reorder with reorderAutomation task
- Added send_ntfy and ticket_assigned trigger types
- Fixed XSS: replaced innerHTML with safe DOM methods for builder rows
2026-06-09 10:51:12 -05:00
gitea-actions[bot] 127aea5e5b chore(version): pre-release bump to 02.34.61-dev [skip ci] 2026-06-09 15:47:54 +00:00
Jonathan Miller 3047327d2e feat(config): complete component options UI (#149)
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 / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) 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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (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 15s
Universal: PR Check / Validate PR (pull_request) Failing after 16s
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 32s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 35s
- General: brand name, support email
- Notifications: ntfy push settings (server, topic, security topic, token)
  with showon conditional display when ntfy is enabled
- Helpdesk: satisfaction ratings toggle, max attachment size
- All ntfy fields map to NotificationService config keys
2026-06-09 10:47:35 -05:00
gitea-actions[bot] 370505d4a2 chore(version): pre-release bump to 02.34.60-dev [skip ci] 2026-06-09 15:46:13 +00:00
Jonathan Miller 45077671fa feat(api): helpdesk tickets REST API endpoints (#142)
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
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
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 12s
Universal: PR Check / Validate PR (pull_request) Failing after 21s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 45s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
- GET /v1/mokosuite/tickets — list with status/category/assigned filters
- GET /v1/mokosuite/tickets/{id} — single ticket with replies + attachments
- POST /v1/mokosuite/tickets — create ticket
- PATCH /v1/mokosuite/tickets/{id} — update status/priority/category/assignment
- POST /v1/mokosuite/tickets/{id}/reply — add reply with notification
- Routes registered in plg_webservices_mokosuite
2026-06-09 10:45:27 -05:00
gitea-actions[bot] 93f9a0f4a2 chore(version): pre-release bump to 02.34.59-dev [skip ci] 2026-06-09 15:42:57 +00:00
Jonathan Miller fbb467a832 feat(helpdesk): satisfaction ratings on resolved tickets (#140)
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.1) (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 2: Unit Tests (8.2) (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 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) 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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: PR Check / Validate PR (pull_request) Failing after 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 31s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 32s
- satisfaction_rating (1-5), satisfaction_feedback, satisfaction_rated_at columns
- Star rating widget in ticket sidebar (appears when resolved/closed)
- Hover highlight + click to select rating
- Optional feedback textarea
- rateTicket controller task persists rating via AJAX
- Displays existing rating with stars + feedback when already rated
2026-06-09 10:42:39 -05:00
gitea-actions[bot] 86a93837f6 chore(version): pre-release bump to 02.34.58-dev [skip ci] 2026-06-09 15:35:08 +00:00
Jonathan Miller 57534eec9c feat(helpdesk): file attachments on tickets and replies (#141)
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
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
- #__mokosuite_ticket_attachments table
- AttachmentService: upload, download, delete with type/size validation
- Allowed: jpg,png,gif,webp,pdf,doc,docx,xls,xlsx,csv,txt,zip (10MB max)
- Secure storage in /media/com_mokosuite/attachments/{ticket_id}/
- Upload field in reply form, auto-uploads after reply creation
- Download links on ticket and reply cards
- Controller tasks: uploadAttachment, downloadAttachment, deleteAttachment
2026-06-09 10:34:49 -05:00
gitea-actions[bot] c999cc67c4 chore(version): pre-release bump to 02.34.57-dev [skip ci] 2026-06-09 15:26:02 +00:00
Jonathan Miller cc3d0df2c2 feat(helpdesk): add drag-and-drop reorder to categories (#139)
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
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
- Drag handle column with icon-menu grip
- HTML5 native drag-and-drop on table rows
- reorderCategory controller task persists ordering via AJAX
2026-06-09 10:25:44 -05:00
gitea-actions[bot] b61e453433 chore(version): pre-release bump to 02.34.56-dev [skip ci] 2026-06-09 15:22:03 +00:00
Jonathan Miller 510d3f1f7d feat(helpdesk): complete canned responses admin UI (#138)
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
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
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 (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
- Edit existing responses via modal (click card to edit)
- Category filter dropdown in list view
- Drag-and-drop reorder with AJAX persistence
- reorderCanned controller task for ordering
- Category badges on response cards
2026-06-09 10:21:43 -05:00
gitea-actions[bot] c1aa9d5213 chore(version): pre-release bump to 02.34.55-dev [skip ci] 2026-06-08 10:22:47 +00:00
Jonathan Miller 05be465f96 feat(notifications): add ntfy push for ticket and security events (#205)
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
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Adds pushNtfy() alongside existing email notifications in
NotificationService. Sends push via ntfy HTTP API for ticket_created,
ticket_replied, status_changed, ticket_assigned events with emoji
tags, priority levels, and click-through URLs.

Security alerts also pushed via ntfy at priority 5 (urgent).

Configurable via component params: ntfy_enabled, ntfy_server,
ntfy_topic, ntfy_security_topic, ntfy_token.
2026-06-08 05:22:30 -05:00
gitea-actions[bot] 0183a8dd3e chore(version): pre-release bump to 02.34.54-dev [skip ci] 2026-06-08 10:11:46 +00:00
Jonathan Miller a4d4a39b97 docs: update changelog with dbip plugin, menu restructure, rc-revert
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
2026-06-08 05:11:26 -05:00
gitea-actions[bot] d2ba5d7123 chore(version): pre-release bump to 02.34.53-dev [skip ci] 2026-06-08 09:29:21 +00:00
Jonathan Miller f52df1912d ci: add rc-revert workflow for release candidate rollbacks
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
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
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
2026-06-08 04:26:57 -05:00
Jonathan Miller 4e797a5f74 feat(dbip): add IP geolocation plugin using DB-IP
New system plugin plg_system_mokosuite_dbip provides IP geolocation
via DB-IP MMDB databases. Supports CDN auto-download of city DB,
local MMDB file mode, and bundled MaxMind DB reader library.
Registered in package manifest.
2026-06-08 04:26:56 -05:00
Jonathan Miller 6aee7353b9 feat(menu): restructure sidebar — each component gets own section
Each installed Moko component now renders as its own top-level
collapsible section instead of being nested under a single MokoSuite
parent. com_mokosuitehq is pinned first, com_mokosuite uses static
views as children, all others auto-discover from #__menu.
2026-06-08 04:26:55 -05:00
gitea-actions[bot] 82c3e96759 chore(version): pre-release bump to 02.34.52-dev [skip ci] 2026-06-07 18:04:48 +00:00
gitea-actions[bot] 6f84af130d chore(version): pre-release bump to 02.34.51-dev [skip ci] 2026-06-07 17:39:01 +00:00
Jonathan Miller ef0a10c262 ci: add platform_detect.php to pre-release workflow detect step
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
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
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 (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 12:38:44 -05:00
gitea-actions[bot] 31737b7820 chore(version): pre-release bump to 02.34.50-dev [skip ci] 2026-06-07 16:55:09 +00:00
Jonathan Miller f0ad9e6ef2 fix: prevent double version bump on fix/patch branch merges to main
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
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Fix branches already have their version bumped by pre-release (patch).
Auto-release now uses --bump none for these, just stripping the -dev
suffix to publish as stable. Feature/dev branches still bump minor.
2026-06-07 11:54:45 -05:00
gitea-actions[bot] 6cad37cb6c chore(version): pre-release bump to 02.34.49-dev [skip ci] 2026-06-07 16:53:22 +00:00
Jonathan Miller b096a03dfd ci: trigger pre-release builds on fix/patch/hotfix/bugfix branches
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
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
2026-06-07 11:52:58 -05:00
gitea-actions[bot] 5798b0a2ee chore(version): pre-release bump to 02.34.48-dev [skip ci] 2026-06-07 16:45:46 +00:00
Jonathan Miller d946d3b79f feat: auto-detect version bump level from PR source branch
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
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
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 (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Patch branches (fix/*, patch/*, hotfix/*, bugfix/*) bump patch version.
All other branches (dev, feature/*, rc) bump minor version.
2026-06-07 11:45:17 -05:00
gitea-actions[bot] be4a9f34ec chore(version): pre-release bump to 02.34.47-dev [skip ci] 2026-06-07 16:19:35 +00:00
Jonathan Miller a00b1647d4 fix: resolve PR review critical issues — status ID-based updates, SLA checks, counts
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
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
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
- updateStatus() now accepts status ID, validates against lookup table,
  writes both status and status_id columns, uses is_closed flag
- Added automation recursion guard to prevent infinite loops
- getStatusCounts() now joins on statuses table (dynamic, not hardcoded)
- getOverdueTickets() uses is_closed instead of ENUM string comparison
- Template SLA checks use status_is_closed instead of in_array()
- Status summary cards rendered dynamically from lookup table
2026-06-07 11:19:14 -05:00
gitea-actions[bot] 0c7a35bd57 chore(version): pre-release bump to 02.34.46-dev [skip ci] 2026-06-07 16:12:51 +00:00
Jonathan Miller e95809ba61 fix: transfer download key from old pkg_mokowaas update site during migration
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
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
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
saveDownloadKey() now checks pkg_mokowaas update site as fallback when
pkg_mokosuite has no dlid, ensuring license keys survive the rename.
2026-06-07 11:12:32 -05:00
gitea-actions[bot] 6dae483607 chore(version): pre-release bump to 02.34.45-dev [skip ci] 2026-06-07 16:10:46 +00:00
Jonathan Miller 0c5be6ab82 docs: update changelog with all unreleased features
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-06-07 11:10:30 -05:00
gitea-actions[bot] b8c03dfa73 chore(version): pre-release bump to 02.34.44-dev [skip ci] 2026-06-07 16:09:12 +00:00
Jonathan Miller b2b9ab4344 feat: customizable statuses/priorities, custom fields, and field groups per category
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Replace hardcoded ENUM status/priority with lookup tables
(#__mokosuite_ticket_statuses, #__mokosuite_ticket_priorities) so admins
can add custom values with colors and ordering. Integrate Joomla's
native custom fields (com_mokosuite.ticket context) with a junction
table linking field groups to ticket categories. Includes migration SQL
to populate new columns from existing ENUM data.
2026-06-07 10:18:33 -05:00
Jonathan Miller 6fedd0e993 feat: ticket contact linking, multi-assignee, and push-based pre-release
Tickets can now link to Joomla contact records (optional contact_id FK).
Replaces single assigned_to with junction table for multi-assignee
support — tickets can be assigned to multiple users and/or user groups.
Updates pre-release workflow to trigger on push to dev/alpha/beta/rc.
2026-06-07 10:05:06 -05:00
Jonathan Miller 2d9454ea3f feat: migrate params from MokoWaaS extensions and remove old entries
Copies params from all mokowaas_* extension entries to their mokosuite_*
equivalents (plugins, modules, component, package), then unprotects and
deletes old extension records, update sites, and filesystem remnants.
2026-06-07 09:38:16 -05:00
Jonathan Miller 49dd26ef0a feat: add Font Awesome 7 to admin backend and MokoWaaS table migration
Load FA7 in admin pages — checks MokoOnyx template params for Kit code,
falls back to bundled FA7 Free files, then FA6 CDN. Install script now
migrates mokowaas_* tables to mokosuite_* (create, copy data, drop old).
2026-06-07 09:35:22 -05:00
Jonathan Miller 00d44256b4 refactor: rename MokoWaaS to MokoSuite across entire codebase
Rebrand all 17 sub-extensions from mokowaas to mokosuite naming,
including component, plugins, modules, task plugins, and webservices.
Updates package manifest, workflows, docs, wiki, and issue templates.
Adds new plg_system_mokosuite_license extension.
2026-06-07 09:25:45 -05:00
Jonathan Miller 894853074d Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 22:42:14 -05:00
Jonathan Miller 891eff01ea fix: add base_url manifest XML fallback in install script heartbeat 2026-06-06 22:42:01 -05:00
gitea-actions[bot] 16384d423e chore(version): pre-release bump to 02.34.43-dev [skip ci] 2026-06-07 03:37:15 +00:00
Jonathan Miller c4ac0c23ad feat: show Support PIN on cpanel dashboard module
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Derives MOKO-XXXX-XXXX support PIN from health token and displays
it as a badge in the cpanel header row alongside the version badge.
Also adds base_url manifest XML fallback for Send Heartbeat button.
2026-06-06 22:36:35 -05:00
Jonathan Miller f3df053ab2 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 22:35:04 -05:00
Jonathan Miller e2813d0290 fix: fall back to manifest XML for base_url in Send Heartbeat
Hidden field defaults aren't in database params until plugin is
re-saved. Added same manifest XML fallback used for signing_key.
2026-06-06 22:34:49 -05:00
gitea-actions[bot] 2efa542338 chore(version): pre-release bump to 02.34.42-dev [skip ci] 2026-06-07 03:32:35 +00:00
Jonathan Miller debe79df87 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 22:32:13 -05:00
Jonathan Miller 0189c38f4c fix: restore missing closing brace for provisionHealthEndpoint()
The Grafana heartbeat removal deleted the method's closing brace,
causing a syntax error (unexpected token "private").
2026-06-06 22:31:58 -05:00
gitea-actions[bot] 45f1005392 chore(version): pre-release bump to 02.34.41-dev [skip ci] 2026-06-07 03:28:51 +00:00
Jonathan Miller 314d629bc8 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 22:28:03 -05:00
Jonathan Miller 7f01e650b9 refactor: remove Grafana heartbeat provisioning, replaced by MokoWaaSHQ
Removes handleGrafanaProvisioning(), sendHeartbeat() from core plugin,
and Grafana heartbeat from core plugin install script. MokoWaaSHQ
monitor plugin now handles all heartbeat communication. Updates language
strings to reference MokoWaaSHQ instead of Grafana.
2026-06-06 22:27:53 -05:00
Jonathan Miller 47f3d36517 fix: gitignore site/ should be /site/ to avoid matching tmpl/site/ 2026-06-06 22:20:05 -05:00
gitea-actions[bot] 46266b28c5 chore(version): pre-release bump to 02.34.40-dev [skip ci] 2026-06-07 03:19:45 +00:00
gitea-actions[bot] c610ad6828 chore(version): pre-release bump to 02.34.39-dev [skip ci] 2026-06-07 03:06:19 +00:00
gitea-actions[bot] 3d541bcb24 chore(version): pre-release bump to 02.34.38-dev [skip ci] 2026-06-07 03:01:35 +00:00
gitea-actions[bot] f287fccf4d chore(version): pre-release bump to 02.34.37-dev [skip ci] 2026-06-07 02:13:20 +00:00
Jonathan Miller 7ff04c6e17 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-06-06 21:11:52 -05:00
Jonathan Miller 88266587e4 docs: rename MokoWaaSBase to MokoWaaSHQ in changelog 2026-06-06 21:11:47 -05:00
gitea-actions[bot] ad2aad900e chore(version): pre-release bump to 02.34.36-dev [skip ci] 2026-06-07 01:54:56 +00:00
Jonathan Miller 54e49eca92 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 20:53:32 -05:00
Jonathan Miller 41b5346f53 refactor: rename MokoWaaSBase references to MokoWaaSHQ
Update heartbeat endpoints, menu exclusions, language strings, and all
cross-references to match the MokoWaaSHQ rename.
2026-06-06 20:52:06 -05:00
gitea-actions[bot] 01d28e7b96 chore(version): pre-release bump to 02.34.35-dev [skip ci] 2026-06-07 01:39:40 +00:00
Jonathan Miller bf15a8f8ff Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 20:39:10 -05:00
Jonathan Miller 2cac30fa48 fix: fall back to manifest XML for signing_key default
Hidden field defaults aren't stored in database params until the plugin
is re-saved. All 3 heartbeat paths now read the signing_key default
from the monitor plugin's manifest XML as a fallback.
2026-06-06 20:39:05 -05:00
gitea-actions[bot] 0a3cb0ffe7 chore(version): pre-release bump to 02.34.34-dev [skip ci] 2026-06-07 01:32:51 +00:00
Jonathan Miller 46a2283140 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 20:32:10 -05:00
Jonathan Miller dcff922c56 fix: exclude MokoWaaSBase from stale update site cleanup
cleanupStaleUpdateSites() matched '%MokoWaaS%' which also caught
MokoWaaSBase entries, deleting their update server registration.
Added NOT LIKE exclusions for MokoWaaSBase.
2026-06-06 20:32:05 -05:00
gitea-actions[bot] e310d7f390 chore(version): pre-release bump to 02.34.33-dev [skip ci] 2026-06-07 01:23:29 +00:00
Jonathan Miller 83009472b7 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 20:22:25 -05:00
Jonathan Miller c09cfdfcbf fix: update install script heartbeat to use MokoWaaSBase with RSA
Replaced old Grafana receiver endpoint with MokoWaaSBase heartbeat API.
Includes RSA signature from monitor plugin's signing_key param.
2026-06-06 20:22:13 -05:00
gitea-actions[bot] 992b5a2c0d chore(version): pre-release bump to 02.34.32-dev [skip ci] 2026-06-07 01:21:12 +00:00
Jonathan Miller 1f2913422e Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 20:20:51 -05:00
Jonathan Miller aa6d87462b fix: add RSA signature to Send Heartbeat button request
The manual heartbeat button was not sending the X-MokoWaaS-Signature
header, causing Base to reject with 403 when RSA verification is
enabled.
2026-06-06 20:20:40 -05:00
gitea-actions[bot] a03eabc636 chore(version): pre-release bump to 02.34.31-dev [skip ci] 2026-06-07 01:00:27 +00:00
Jonathan Miller 5c35a1aff8 docs: update changelog with PerfectPublisher removal
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
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 19:59:53 -05:00
Jonathan Miller 6f690816d7 chore: remove PerfectPublisher webservices plugin
No longer needed — removed plugin directory, package manifest entry,
and protected extensions list reference.
2026-06-06 19:59:06 -05:00
Jonathan Miller e1f01be1a9 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 19:57:19 -05:00
Jonathan Miller 2a0e450c53 feat: RSA-signed heartbeat authentication
Client signs domain|timestamp|token with private key (distributed via
monitor plugin manifest). Base verifies with public key in component
config. Replaces shared secret and key ring approaches — no rotation
needed. Includes 5-minute timestamp window for replay protection.
2026-06-06 19:56:26 -05:00
gitea-actions[bot] 1eae4c88a6 chore(version): pre-release bump to 02.34.30-dev [skip ci] 2026-06-06 23:04:49 +00:00
Jonathan Miller de78e66da1 feat: Send Heartbeat button next to token field
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Adds a heart icon button in the token field input group that triggers
an AJAX heartbeat to MokoWaaSBase. Shows spinner while sending,
green check on success, red X on failure. Uses the monitor plugin's
configured base_url and the core plugin's health_api_token.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 17:40:09 -05:00
gitea-actions[bot] 2ff323b920 chore(version): pre-release bump to 02.34.29-dev [skip ci] 2026-06-06 22:37:06 +00:00
Jonathan Miller 0ffafeb247 refactor: remove universal download key handler, use single-key pattern
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
MokoWaaS now only saves/restores its own download key (pkg_mokowaas),
matching the pattern used by all other Moko extensions. Removed the
universal backupDownloadKeys/restoreDownloadKeys and the runtime
preserveDownloadKeys no-op from the core plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 17:36:17 -05:00
gitea-actions[bot] 1a3a125b82 chore(version): pre-release bump to 02.34.28-dev [skip ci] 2026-06-06 21:26:39 +00:00
Jonathan Miller 77c160a64e fix: rename migration to 02.34.28 so it runs on current dev version
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 16:25:24 -05:00
Jonathan Miller 473d512e1c refactor: remove database-backed download key table
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
The #__mokowaas_download_keys table approach was over-engineered.
Download key preservation is handled by the install script's
preflight/postflight with element-name matching.

Removed:
- #__mokowaas_download_keys table from install SQL
- syncKeysToTable/applyKeysFromTable from core plugin
- saveDownloadKey/applyDownloadKey/reapplyAllDownloadKeys from model
- ensureDownloadKeysTable/syncKeysToDatabase/reapplyKeysFromDatabase
  from install script

Added:
- Migration SQL (02.35.00) to DROP the table from existing installs
- Uninstall SQL for all component tables
- Install/uninstall SQL blocks in component manifest

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 16:24:20 -05:00
Jonathan Miller 670eda8d91 fix: download key lost on update — stale URL in cleanupStaleUpdateSites
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
The cleanup method used the old /raw/branch/main/updates.xml URL as
its keep target, but migrateUpdateServerUrls had already rewritten
all URLs to /updates.xml. This caused the method to find no match
and delete all MokoWaaS update sites — including the one with the
download key.

Fixed by updating the hardcoded URL to match the current manifest
format: /MokoConsulting/MokoWaaS/updates.xml

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 16:10:48 -05:00
gitea-actions[bot] d9b77d5017 chore(version): pre-release bump to 02.34.27-dev [skip ci] 2026-06-06 20:55:02 +00:00
Jonathan Miller 104251f800 fix: create download_keys table on update + element-based key matching
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Root cause: the #__mokowaas_download_keys table was only in
install.mysql.sql (fresh installs). Existing sites never got it,
so syncKeysToTable/applyKeysFromTable silently failed.

- Add sql/updates/mysql/02.35.00.sql migration for existing installs
- Register <update><schemas> in component manifest
- Add ensureDownloadKeysTable() as belt-and-suspenders in postflight
- backupDownloadKeys() now saves by element name (stable identifier)
- restoreDownloadKeys() matches by element first, URL second, ID third

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:53:42 -05:00
Jonathan Miller 99398fde6b Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 15:48:16 -05:00
Jonathan Miller 01c0bb8a32 fix: restore download keys by element name, not just URL/ID
The URL migration in postflight changes update site URLs BEFORE
restoreDownloadKeys runs, so URL-based matching fails. Element names
are stable across updates. Now backs up keys as elem_ELEMENT and
restores by matching the extension element name first, falling back
to URL and ID.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:47:38 -05:00
gitea-actions[bot] 4d6a53f6e7 chore(version): pre-release bump to 02.34.26-dev [skip ci] 2026-06-06 20:44:21 +00:00
Jonathan Miller 2656db9579 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 15:40:43 -05:00
Jonathan Miller 2ede22282d fix: extension manager cards fill full width when 1-2 items 2026-06-06 15:40:16 -05:00
gitea-actions[bot] 48905790f0 chore(version): pre-release bump to 02.34.25-dev [skip ci] 2026-06-06 20:32:32 +00:00
Jonathan Miller 3cf0773fd6 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 15:31:23 -05:00
Jonathan Miller fc068866d9 feat: database-backed download key preservation for all extensions
Replace JSON file backup with #__mokowaas_download_keys table as
the persistent single source of truth for download keys.

- Core plugin: syncKeysToTable() copies keys from Joomla to our table,
  applyKeysFromTable() re-applies from our table to Joomla. Runs on
  every admin page load — Joomla can wipe keys all it wants.
- Install script: preflight saves to table, postflight re-applies.
- ExtensionsModel: saveDownloadKey(), applyDownloadKey(),
  reapplyAllDownloadKeys() static method for install/update hooks.
- Extension manager: prompt for download key on install, skip
  extensions with no release, show missing key warning badge.
- Catalog: expanded to 11 Joomla extensions.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:31:04 -05:00
gitea-actions[bot] dacf707165 chore(version): pre-release bump to 02.34.24-dev [skip ci] 2026-06-06 20:20:10 +00:00
Jonathan Miller 76f9da07a9 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 15:18:51 -05:00
Jonathan Miller 38dd78fdab fix: preserveDownloadKeys matches by URL when IDs change
When Joomla deletes and recreates update site rows, they get new IDs.
The backup was keyed only by ID, so restored keys couldn't match the
new rows. Now stores keys by both ID and URL (url:https://...) and
looks up by URL as fallback when the ID doesn't match.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:18:36 -05:00
gitea-actions[bot] 3b5b0c1a73 chore(version): pre-release bump to 02.34.23-dev [skip ci] 2026-06-06 20:15:22 +00:00
Jonathan Miller 5ab496b399 fix: backup download keys in preflight, not postflight
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Joomla's package installer deletes and recreates update site rows
from the manifest BETWEEN preflight and postflight. By the time
postflight ran backupDownloadKeys(), the extra_query values were
already empty.

Moved the backup to preflight() via a class property. The restore
in postflight() now uses keys saved before Joomla touched them.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:14:18 -05:00
gitea-actions[bot] 64a11706fd chore(version): pre-release bump to 02.34.22-dev [skip ci] 2026-06-06 20:09:45 +00:00
Jonathan Miller 969f7fb615 feat: expanded catalog with all Joomla repos + download key warning
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
- catalog.xml: added MokoWaaSBase, MokoJoomBackup, MokoJoomCommunity,
  MokoJoomCross, MokoJoomStoreLocator (11 total extensions)
- ExtensionsModel: fetchFromUpdateServer() now returns has_stable,
  has_dev, needs_dlid flags from updates.xml parsing
- ExtensionsModel: hasDownloadKey() checks if dlid is configured
  in update_sites for each extension
- Template: red alert badge when download key is missing on installed
  extensions that require one for updates

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:08:46 -05:00
Jonathan Miller 3c28483faf feat: Sprint 4 — Customer portal with orders, invoices, e-sign, license (#192)
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
All net-new site-side files (19 files). No existing views modified.

Portal Dashboard:
- KPI cards: open orders, unpaid invoices, open tickets, pending signatures
- Recent orders table, quick links to all portal sections
- User matched to ERP contact by email address

My Orders (list + detail):
- Customer's order list with status/payment badges
- Order detail with line items, subtotal/tax/total

My Invoices (list + detail):
- Invoice list with overdue highlighting, balance due
- Invoice detail with line items and payment totals

E-Signature Public Signing Page:
- Token-based access (no Joomla login required)
- Consent checkbox (must accept before signing)
- HTML5 Canvas signature pad with touch/stylus/mouse support
- High-DPI canvas rendering
- Geolocation capture on submit
- Decline with reason
- signature-pad.js: full signing flow with consent → sign → success

E-Signature Verification Page:
- Hash-based public verification (no auth)
- Document status, signer table, complete audit trail

License Portal:
- Current license package, status, active services
- DLID entry/update form for self-service license management

Assets:
- portal.css: shared portal styles
- signature-pad.js: Canvas drawing with touch events, DPI scaling

1 model: PortalModel (resolves user→contact, loads orders/invoices/dashboard)
2026-06-06 15:04:25 -05:00
Jonathan Miller 8a2df44865 fix: preserve download keys during install script update site cleanup
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
backupDownloadKeys() saves all extra_query values before migration and
cleanup operations. restoreDownloadKeys() re-applies them after, matching
by URL first then by old ID. Also updates the file-based backup used by
the runtime preserveDownloadKeys() guard.

Root cause: cleanupStaleUpdateSites() deletes update site rows which
destroys their extra_query (dlid) values. The rows get recreated by
Joomla but without the download keys.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:00:52 -05:00
gitea-actions[bot] 23ccbcbeae chore(version): pre-release bump to 02.34.21-dev [skip ci] 2026-06-06 19:52:48 +00:00
jmiller 696ffefc1c chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-06 19:50:33 +00:00
jmiller 1a33542f20 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 19:48:07 +00:00
gitea-actions[bot] caa1a2a96e chore(version): pre-release bump to 02.34.20-dev [skip ci] 2026-06-06 19:07:11 +00:00
Jonathan Miller 1229f111e8 docs: update CHANGELOG for 02.35.00 release
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-06-06 14:06:06 -05:00
gitea-actions[bot] fa893e8713 chore(version): pre-release bump to 02.34.19-dev [skip ci] 2026-06-06 18:42:12 +00:00
gitea-actions[bot] f586175be2 chore(version): pre-release bump to 02.34.18-dev [skip ci] 2026-06-06 18:00:43 +00:00
Jonathan Miller 55b4f994dc fix: add manifest_element.php to pre-installed tools check
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 12:54:08 -05:00
gitea-actions[bot] 2d0ec0bca8 chore(version): pre-release bump to 02.34.17-dev [skip ci] 2026-06-06 17:48:18 +00:00
Jonathan Miller fc32dbe8ab fix: workflow quoting fix for act_runner shell compatibility
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: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Synced from moko-platform — removes double quotes that act_runner
interprets literally, breaking git clone URL construction.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 12:46:40 -05:00
303 changed files with 11874 additions and 6406 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ releases/
build/ build/
dist/ dist/
out/ out/
site/ /site/
!source/packages/*/site/ !source/packages/*/site/
*.map *.map
*.css.map *.css.map
+21 -21
View File
@@ -1,4 +1,4 @@
# MokoWaaS # MokoSuite
Joomla 5/6 admin tools suite — heartbeat health monitoring, extension management, security firewall, tenant restrictions, and site administration. Joomla 5/6 admin tools suite — heartbeat health monitoring, extension management, security firewall, tenant restrictions, and site administration.
@@ -6,10 +6,10 @@ Joomla 5/6 admin tools suite — heartbeat health monitoring, extension manageme
| Field | Value | | Field | Value |
|---|---| |---|---|
| **Package** | `pkg_mokowaas` | | **Package** | `pkg_mokosuite` |
| **Language** | PHP 8.1+ | | **Language** | PHP 8.1+ |
| **Branch** | develop on `dev`, merge to `main` (protected) | | **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) | | **Wiki** | [MokoSuite Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki) |
## Commands ## Commands
@@ -19,38 +19,38 @@ composer install # Install PHP dependencies
## Architecture ## Architecture
Joomla **package** (`pkg_mokowaas`) with 17 sub-extensions: Joomla **package** (`pkg_mokosuite`) with 17 sub-extensions:
### Core Plugin (`plg_system_mokowaas`) ### Core Plugin (`plg_system_mokosuite`)
- Heartbeat health endpoint (`/?mokowaas=health`) with 16 diagnostic checks - Heartbeat health endpoint (`/?mokosuite=health`) with 16 diagnostic checks
- Grafana provisioning and heartbeat sender - Grafana provisioning and heartbeat sender
- Site alias / domain management - Site alias / domain management
- Extension cascade (enable/disable coordination) - Extension cascade (enable/disable coordination)
- Download key preservation across Joomla updates - Download key preservation across Joomla updates
- Namespace: `Moko\Plugin\System\MokoWaaS` - Namespace: `Moko\Plugin\System\MokoSuite`
### Feature Plugins ### Feature Plugins
- `plg_system_mokowaas_firewall` — WAF, IP blocklist, security headers, password policy - `plg_system_mokosuite_firewall` — WAF, IP blocklist, security headers, password policy
- `plg_system_mokowaas_tenant` — admin restrictions for non-master users - `plg_system_mokosuite_tenant` — admin restrictions for non-master users
- `plg_system_mokowaas_devtools` — dev mode, hit reset, version cleanup, download key reset - `plg_system_mokosuite_devtools` — dev mode, hit reset, version cleanup, download key reset
- `plg_system_mokowaas_offline` — offline mode bypass for legal pages - `plg_system_mokosuite_offline` — offline mode bypass for legal pages
- `plg_system_mokowaas_monitor` — Grafana heartbeat registration - `plg_system_mokosuite_monitor` — Grafana heartbeat registration
### Component (`com_mokowaas`) ### Component (`com_mokosuite`)
- Admin dashboard with plugin management, WAF charts, extension catalog - Admin dashboard with plugin management, WAF charts, extension catalog
- Helpdesk ticketing system - Helpdesk ticketing system
- REST API controllers - REST API controllers
### Modules ### Modules
- `mod_mokowaas_cpanel` — admin dashboard widget - `mod_mokosuite_cpanel` — admin dashboard widget
- `mod_mokowaas_menu` — admin sidebar menu - `mod_mokosuite_menu` — admin sidebar menu
- `mod_mokowaas_cache` — status bar cache/temp cleaner - `mod_mokosuite_cache` — status bar cache/temp cleaner
- `mod_mokowaas_categories` — auto-category tree menu - `mod_mokosuite_categories` — auto-category tree menu
### Task Plugins ### Task Plugins
- `plg_task_mokowaasdemo` — scheduled demo site reset - `plg_task_mokosuitedemo` — scheduled demo site reset
- `plg_task_mokowaassync` — scheduled content sync - `plg_task_mokosuitesync` — scheduled content sync
- `plg_task_mokowaas_tickets` — ticket automation - `plg_task_mokosuite_tickets` — ticket automation
### Update Server ### Update Server
@@ -59,7 +59,7 @@ MokoGitea generates update feeds dynamically from releases — no static `update
## Source Directory ## Source Directory
Source lives in `source/` (not `src/`): Source lives in `source/` (not `src/`):
- `source/pkg_mokowaas.xml` — package manifest - `source/pkg_mokosuite.xml` — package manifest
- `source/script.php` — install script - `source/script.php` — install script
- `source/packages/` — all sub-extensions - `source/packages/` — all sub-extensions
@@ -1,6 +1,6 @@
--- ---
name: WaaS Client Site Issue name: Suite Client Site Issue
about: Report an issue with a WaaS client site (branding, deployment, media sync) about: Report an issue with a Suite client site (branding, deployment, media sync)
title: '[WAAS] ' title: '[WAAS] '
labels: 'waas, client-site' labels: 'waas, client-site'
assignees: '' assignees: ''
@@ -52,7 +52,7 @@ Attach screenshots showing the issue (desktop and mobile if relevant).
## Template Details ## Template Details
- **Joomla Version**: [e.g., 5.x] - **Joomla Version**: [e.g., 5.x]
- **Template Name**: [e.g., clienttemplate] - **Template Name**: [e.g., clienttemplate]
- **MokoWaaS Plugin**: [Active / Inactive] - **MokoSuite Plugin**: [Active / Inactive]
- **MokoOnyx Admin**: [Active / Inactive] - **MokoOnyx Admin**: [Active / Inactive]
## CSS Custom Properties ## CSS Custom Properties
+21 -21
View File
@@ -11,9 +11,9 @@ INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoStandards REPO: https://github.com/mokoconsulting-tech/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories BRIEF: GitHub Copilot custom instructions template for Joomla/MokoSuite governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync. NOTE: Synced to .github/copilot-instructions.md in all Joomla/Suite repos via bulk sync.
Tokens replaced at sync time: MokoWaaS, https://github.com/mokoconsulting-tech/MokoWaaS, {{EXTENSION_NAME}}, Tokens replaced at sync time: MokoSuite, https://github.com/mokoconsulting-tech/MokoSuite, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}} {{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
--> -->
@@ -36,24 +36,24 @@ NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bul
> >
> | Placeholder | Where to find the value | > | Placeholder | Where to find the value |
> |---|---| > |---|---|
> | `MokoWaaS` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | > | `MokoSuite` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://github.com/mokoconsulting-tech/MokoWaaS` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` | > | `https://github.com/mokoconsulting-tech/MokoSuite` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root | > | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) | > | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) | > | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
> >
> --- > ---
# MokoWaaS — GitHub Copilot Custom Instructions # MokoSuite — GitHub Copilot Custom Instructions
## What This Repo Is ## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. This is a **Moko Consulting MokoSuite** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://github.com/mokoconsulting-tech/MokoWaaS Repository URL: https://github.com/mokoconsulting-tech/MokoSuite
Extension name: **{{EXTENSION_NAME}}** Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`) Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Platform: **Joomla 4.x / MokoWaaS** Platform: **Joomla 4.x / MokoSuite**
--- ---
@@ -77,9 +77,9 @@ Every new file needs a copyright header as its first content.
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: MokoWaaS.{{EXTENSION_TYPE}} * DEFGROUP: MokoSuite.{{EXTENSION_TYPE}}
* INGROUP: MokoWaaS * INGROUP: MokoSuite
* REPO: https://github.com/mokoconsulting-tech/MokoWaaS * REPO: https://github.com/mokoconsulting-tech/MokoSuite
* PATH: /path/to/file.php * PATH: /path/to/file.php
* VERSION: XX.YY.ZZ * VERSION: XX.YY.ZZ
* BRIEF: One-line description of purpose * BRIEF: One-line description of purpose
@@ -98,9 +98,9 @@ This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: MokoWaaS.Documentation DEFGROUP: MokoSuite.Documentation
INGROUP: MokoWaaS INGROUP: MokoSuite
REPO: https://github.com/mokoconsulting-tech/MokoWaaS REPO: https://github.com/mokoconsulting-tech/MokoSuite
PATH: /docs/file.md PATH: /docs/file.md
VERSION: XX.YY.ZZ VERSION: XX.YY.ZZ
BRIEF: One-line description BRIEF: One-line description
@@ -138,7 +138,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
<version>01.02.04</version> <version>01.02.04</version>
<downloads> <downloads>
<downloadurl type="full" format="zip"> <downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoWaaS/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip https://github.com/mokoconsulting-tech/MokoSuite/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl> </downloadurl>
</downloads> </downloads>
<targetplatform name="joomla" version="4\.[0-9]+" /> <targetplatform name="joomla" version="4\.[0-9]+" />
@@ -152,7 +152,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
## Joomla Extension Structure ## Joomla Extension Structure
``` ```
MokoWaaS/ MokoSuite/
├── manifest.xml # Joomla installer manifest (root — required) ├── manifest.xml # Joomla installer manifest (root — required)
├── (no updates.xml) # Update XML is generated dynamically by MokoGitea ├── (no updates.xml) # Update XML is generated dynamically by MokoGitea
├── site/ # Frontend (site) code ├── site/ # Frontend (site) code
@@ -191,11 +191,11 @@ MokoWaaS/
https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml
``` ```
The package manifest (`pkg_mokowaas.xml`) references it via: The package manifest (`pkg_mokosuite.xml`) references it via:
```xml ```xml
<updateservers> <updateservers>
<server type="extension" priority="1" name="MokoWaaS Update Server"> <server type="extension" priority="1" name="MokoSuite Update Server">
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/updates.xml
</server> </server>
</updateservers> </updateservers>
``` ```
@@ -257,7 +257,7 @@ This repository is governed by [MokoStandards](https://github.com/mokoconsulting
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | | [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | | [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide | | [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoSuite Joomla extension development guide |
--- ---
+4 -4
View File
@@ -5,11 +5,11 @@
--> -->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0"> <moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity> <identity>
<name>MokoWaaS</name> <name>MokoSuite</name>
<display-name>Package - MokoWaaS</display-name> <display-name>Package - MokoSuite</display-name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description> <description>White-label identity, security hardening, and tenant restriction layer for Suite-managed Joomla environments</description>
<version>02.35.00</version> <version>02.34.79</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
+53 -28
View File
@@ -17,7 +17,7 @@
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
# | Platform-specific: | # | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages | # | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
@@ -71,16 +71,21 @@ jobs:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
if ! command -v composer &> /dev/null; then if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Rename branch to rc - name: Rename branch to rc
run: | run: |
@@ -102,14 +107,13 @@ jobs:
run: | run: |
php ${MOKO_CLI}/release_publish.php \ php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \ --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}"
--skip-update-stream
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release: release:
@@ -136,7 +140,7 @@ jobs:
run: | run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found - aborting release" echo "::error::Merge conflict markers found aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
@@ -151,23 +155,44 @@ jobs:
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: | run: |
if ! command -v composer &> /dev/null; then if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ - name: "Determine version bump level"
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ id: bump
/tmp/moko-platform-api run: |
cd /tmp/moko-platform-api # Fix/patch branches: version was already bumped by pre-release, just strip suffix
composer install --no-dev --no-interaction --quiet # Feature/dev branches: bump minor for the new stable release
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;;
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Publish stable release" - name: "Publish stable release"
run: | run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \ php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \ --path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}"
--skip-update-stream
- name: Update release notes from CHANGELOG.md - name: Update release notes from CHANGELOG.md
run: | run: |
@@ -244,7 +269,7 @@ jobs:
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral - created by promote-rc) # Delete rc branch (ephemeral created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \ "${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found" && echo "Deleted rc branch" || echo "rc branch not found"
@@ -301,7 +326,7 @@ jobs:
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "## Already Released ${VERSION}" >> $GITHUB_STEP_SUMMARY
else else
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+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.35.00 # VERSION: 02.34.79
# 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"
File diff suppressed because it is too large Load Diff
+35 -25
View File
@@ -8,19 +8,21 @@
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
pull_request: push:
types: [closed]
branches: branches:
- dev - dev
pull_request_target: - 'fix/**'
types: [synchronize, opened, reopened] - 'patch/**'
branches: - 'hotfix/**'
- main - 'bugfix/**'
- alpha
- beta
- rc
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
@@ -43,12 +45,11 @@ env:
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})" name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release runs-on: release
if: >- if: >-
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || github.event_name == 'push'
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps: steps:
- name: Checkout - name: Checkout
@@ -56,7 +57,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} ref: ${{ github.ref_name }}
- name: Setup moko-platform tools - name: Setup moko-platform tools
env: env:
@@ -64,33 +65,39 @@ jobs:
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h) # Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else else
echo Falling back to fresh clone echo Falling back to fresh clone
if ! command -v composer &> /dev/null; then if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi fi
rm -rf /tmp/moko-platform-api rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi fi
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
run: | run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development # Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then if [ "${{ github.event_name }}" = "push" ]; then
STABILITY="release-candidate" case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else else
STABILITY="${{ inputs.stability || 'development' }}" STABILITY="${{ inputs.stability || 'development' }}"
fi fi
@@ -118,6 +125,9 @@ jobs:
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output # Append suffix for output
if [ -n "$SUFFIX" ]; then if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}" VERSION="${VERSION}${SUFFIX}"
@@ -162,7 +172,7 @@ jobs:
php ${MOKO_CLI}/release_create.php \ php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \ --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md - name: Update release notes from CHANGELOG.md
run: | run: |
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoPlatform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
+90 -29
View File
@@ -11,10 +11,10 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: DEFGROUP:
INGROUP: MokoWaaS.Documentation INGROUP: MokoSuite.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
PATH: ./CHANGELOG.md PATH: ./CHANGELOG.md
VERSION: 02.35.00 VERSION: 02.34.79
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
@@ -22,23 +22,84 @@
## [Unreleased] ## [Unreleased]
### Added
- plg_system_mokosuite_dbip — IP geolocation plugin using DB-IP MMDB databases (CDN auto-download, local file mode, bundled MaxMind reader)
- Admin sidebar menu restructure — each Moko component gets its own collapsible section, com_mokosuitehq pinned first
- rc-revert workflow for release candidate rollbacks
- Ntfy push notifications for ticket events and security alerts (#205) — configurable server/topic/token
- Canned responses admin UI with edit modal, category filter, drag-and-drop reorder (#138)
- Ticket categories drag-and-drop reorder (#139)
- File attachments on tickets and replies (#141) — upload/download/delete with type and size validation
- Satisfaction ratings on resolved tickets (#140) — 1-5 star widget with optional feedback
- Helpdesk REST API (#142) — GET/POST/PATCH tickets, POST replies, filters, pagination
- Component config options UI (#149) — general, notification (email + ntfy), helpdesk settings
- Automation rule visual builder (#137) — condition/action dropdowns, edit existing, reorder, XSS-safe DOM
- Admin login and failed login security notifications (#147)
- Automation engine with Joomla event triggers (#151) — user_login, user_register, content_save, extension_install, behavior modes (append/always_new/skip_if_open), create_ticket action
- RSA-signed heartbeat authentication — private key in monitor plugin manifest, public key on MokoSuiteHQ
- Monitor plugin base_url set via manifest (hidden from admin UI), propagated via update server
- Send Heartbeat button on health token field for manual heartbeat testing
- Font Awesome 7 loaded in admin backend — picks up MokoOnyx Kit code if present, falls back to bundled FA7 Free or FA6 CDN
- MokoWaaS → MokoSuite database table migration in install script (create new, copy data, drop old)
- MokoWaaS → MokoSuite extension param migration — copies params from all old mokowaas plugins/modules/component, then removes old entries and filesystem remnants
- Ticket contact linking — optional FK to Joomla contact records with display in list and detail views
- Multi-assignee tickets — junction table supports multiple users and user groups per ticket
- Customizable ticket statuses — admin-configurable lookup table replaces hardcoded ENUM (title, color, is_closed flag)
- Customizable ticket priorities — admin-configurable lookup table with weight and color
- Joomla custom fields integration for tickets (context: com_mokosuite.ticket) with field groups assignable per category
- MokoWaaS/MokoWaaSHQ migration bridge repos with updates.xml redirecting existing installs to MokoSuite/HQ
- Pre-release workflow triggers on push to dev/alpha/beta/rc branches (deployed to all 11 repos)
## [02.35.00] --- 2026-06-06 ### Removed
- PerfectPublisher webservices plugin (no longer needed)
### Fixed
- Download key lost on update: cleanupStaleUpdateSites used old /raw/branch/main/ URL format, deleting the manifest-registered update site that held the key
## [02.35.00] - 2026-06-06
### Added
- Core plugin stripped to heartbeat-only config (~5,500 lines removed)
- Extension catalog (catalog.xml) with update server discovery (#186)
- Download key preservation across Joomla updates (#187)
- Remote login endpoint for MokoSuiteHQ auto-login
- Provision reset API for new client setup (hits, versions, tokens)
- Setup required banner after provision reset
- Support verification PIN (MOKO-XXXX-XXXX)
- mod_mokosuite_categories — auto-category tree menu (#184)
- Cache/temp split button in status bar
- Dashboard version tiles for component and modules
- Monitor plugin sends full health payload to MokoSuiteHQ
- Firewall: block_frontend_superuser, own trusted_ip_entry.xml
- DevTools: reset download keys toggle
### Changed
- Renamed src/ to source/ (#188)
- Service classes relocated to owning plugins
- API controller execute() signatures fixed (#183)
- Joomla 5/6 event compatibility in DevTools and Monitor
- Dead placeholder resolver removed from install script
### Fixed
- Firewall subform paths after core cleanup
- Missing Security Headers language strings
## [02.34.00] - 2026-06-04
### Added ### Added
- Database Tools view — table status, optimize, repair, session purge (#127) - Database Tools view — table status, optimize, repair, session purge (#127)
- Cache Cleanup view — directory size reporting and one-click cleanup (#128) - Cache Cleanup view — directory size reporting and one-click cleanup (#128)
- mod_mokowaas_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner) - mod_mokosuite_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
- mod_mokowaas_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder) - mod_mokosuite_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder)
- SSL certificate expiry monitoring in cpanel module (#148) - SSL certificate expiry monitoring in cpanel module (#148)
- MokoWaaS-specific update badge (blue) separate from other updates in cpanel module - MokoSuite-specific update badge (blue) separate from other updates in cpanel module
- migrateUpdateServerUrls() — rewrites all Moko extension update server URLs to clean /updates.xml on install/update - migrateUpdateServerUrls() — rewrites all Moko extension update server URLs to clean /updates.xml on install/update
- fixMenuIcons() — sets menu_icon params on submenu items (Joomla only renders img on level 1) - fixMenuIcons() — sets menu_icon params on submenu items (Joomla only renders img on level 1)
- setupCacheModule() — registers cache cleaner module in status bar position on install - setupCacheModule() — registers cache cleaner module in status bar position on install
- Component config.xml for Joomla Options modal (#149) - Component config.xml for Joomla Options modal (#149)
- preflight() ALTER for #__extensions.element default (MySQL strict mode fix) - preflight() ALTER for #__extensions.element default (MySQL strict mode fix)
- Retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar on install - Retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar on install
- MokoJoomTOS settings auto-migrate to mokowaas_offline before removal - MokoJoomTOS settings auto-migrate to mokosuite_offline before removal
- dev-release and pre-release workflows with changelog extraction into release notes - dev-release and pre-release workflows with changelog extraction into release notes
- RC pre-release consolidates dev patches into clean minor version bump - RC pre-release consolidates dev patches into clean minor version bump
@@ -70,20 +131,20 @@
## [02.32] - 2026-06-02 ## [02.32] - 2026-06-02
### Added ### Added
- Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions - Admin control panel dashboard in com_mokosuite with site info bar, feature plugin grid, and quick actions
- Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard - Feature plugin architecture — MokoSuite features split into toggleable plugins managed from the dashboard
- plg_system_mokowaas_firewall — HTTPS enforcement, trusted IPs, session timeout, upload restrictions, password policy - plg_system_mokosuite_firewall — HTTPS enforcement, trusted IPs, session timeout, upload restrictions, password policy
- plg_system_mokowaas_tenant — Installer, sysinfo, config, template, and menu restrictions for non-master users - plg_system_mokosuite_tenant — Installer, sysinfo, config, template, and menu restrictions for non-master users
- plg_system_mokowaas_devtools — Dev mode, hit counter reset, content version cleanup - plg_system_mokosuite_devtools — Dev mode, hit counter reset, content version cleanup
- plg_system_mokowaas_monitor — Grafana heartbeat integration and health monitoring - plg_system_mokosuite_monitor — Grafana heartbeat integration and health monitoring
- MokoWaaSHelper utility class for shared master-user detection across feature plugins - MokoSuiteHelper utility class for shared master-user detection across feature plugins
- AJAX plugin toggle — enable/disable feature plugins directly from the dashboard - AJAX plugin toggle — enable/disable feature plugins directly from the dashboard
- Clear cache quick action on dashboard - Clear cache quick action on dashboard
- Static updates.xml for update server (licensing system deferred) - Static updates.xml for update server (licensing system deferred)
- Automatic param migration from core plugin to feature plugins on upgrade - Automatic param migration from core plugin to feature plugins on upgrade
### Changed ### Changed
- com_mokowaas upgraded from API-only to full admin component with dashboard views - com_mokosuite upgraded from API-only to full admin component with dashboard views
- Package manifest updated with 4 new feature plugin entries (10 extensions total) - Package manifest updated with 4 new feature plugin entries (10 extensions total)
- Update server URL changed to static raw file endpoint - Update server URL changed to static raw file endpoint
- Core plugin slimmed — security, tenant, devtools, and monitor features extracted to dedicated plugins - Core plugin slimmed — security, tenant, devtools, and monitor features extracted to dedicated plugins
@@ -101,7 +162,7 @@
- Persistent admin warning when no license key is configured in Update Sites - Persistent admin warning when no license key is configured in Update Sites
- Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired - Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired
- Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records) - Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records)
- Content sync rewritten — bulk MokoWaaS API endpoints (syncclear + syncpush) replace per-item Joomla API calls - Content sync rewritten — bulk MokoSuite API endpoints (syncclear + syncpush) replace per-item Joomla API calls
- Sync task per-instance config: target URL, health token, content type checkboxes (articles, categories, menus, modules) - Sync task per-instance config: target URL, health token, content type checkboxes (articles, categories, menus, modules)
- Bulk sync completes in under 5 seconds (clear + push in 2-3 HTTP requests) - Bulk sync completes in under 5 seconds (clear + push in 2-3 HTTP requests)
- Asset table and nested set tree repair after sync push on target site - Asset table and nested set tree repair after sync push on target site
@@ -121,7 +182,7 @@
- Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases - Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases
- Basic branding config tab (brand name, company name, support URL) - Basic branding config tab (brand name, company name, support URL)
- Visual branding config tab (colors, icon, custom CSS) - Visual branding config tab (colors, icon, custom CSS)
- WaaS Access config tab (master user toggle, master email) - Suite Access config tab (master user toggle, master email)
- Content Sync config tab (targets now in scheduled tasks) - Content Sync config tab (targets now in scheduled tasks)
- Site Aliases config tab (hardcoded to dev.{primary_domain}) - Site Aliases config tab (hardcoded to dev.{primary_domain})
- File sync (images/, files/, media/) — sync is API/DB content only - File sync (images/, files/, media/) — sync is API/DB content only
@@ -135,24 +196,24 @@
### Fixed ### Fixed
- Emergency access IP whitelist: empty `allowed_ips` now permits all IPs (was blocking everyone) - Emergency access IP whitelist: empty `allowed_ips` now permits all IPs (was blocking everyone)
- Emergency access reads `allowed_ips` from plugin params instead of global config - Emergency access reads `allowed_ips` from plugin params instead of global config
- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites - `plg_task_mokosuitesync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
- Community Builder tables added to demo reset safe table list - Community Builder tables added to demo reset safe table list
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL - API endpoint `POST /api/index.php/v1/mokosuite/install` — install extensions from a remote ZIP URL
- Demo Mode with configurable warning banner on frontend when enabled - Demo Mode with configurable warning banner on frontend when enabled
- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours - Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
- `DemoResetService` — baseline snapshot and restore for DB tables + media files - `DemoResetService` — baseline snapshot and restore for DB tables + media files
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string) - API endpoints `POST /?mokosuite=reset` and `POST /?mokosuite=snapshot` (query-string)
- REST endpoints `POST /api/v1/mokowaas/reset` and `GET/POST /api/v1/mokowaas/snapshot` - REST endpoints `POST /api/v1/mokosuite/reset` and `GET/POST /api/v1/mokosuite/snapshot`
- `plg_task_mokowaasdemo` — Joomla Scheduled Task plugin for automatic demo site reset - `plg_task_mokosuitedemo` — Joomla Scheduled Task plugin for automatic demo site reset
- Admin toggles: Take Snapshot Now and Restore Baseline Now in plugin config - Admin toggles: Take Snapshot Now and Restore Baseline Now in plugin config
- Content Sync: one-way push of articles, categories, menus, and modules to remote MokoWaaS sites - Content Sync: one-way push of articles, categories, menus, and modules to remote MokoSuite sites
- Content Sync: API endpoints `POST /?mokowaas=sync` (sender) and `POST /?mokowaas=sync-receive` (receiver) - Content Sync: API endpoints `POST /?mokosuite=sync` (sender) and `POST /?mokosuite=sync-receive` (receiver)
- Content Sync: REST endpoints `POST /api/v1/mokowaas/sync` and `POST /api/v1/mokowaas/sync-receive` - Content Sync: REST endpoints `POST /api/v1/mokosuite/sync` and `POST /api/v1/mokosuite/sync-receive`
- Content Sync: configurable sync targets with URL + API token in plugin settings - Content Sync: configurable sync targets with URL + API token in plugin settings
- Package installer: protect all MokoWaaS extensions (not just system plugin) and ensure update server stays enabled - Package installer: protect all MokoSuite extensions (not just system plugin) and ensure update server stays enabled
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update - Package installer: clean up legacy `mokosuitebrand` extension entries and files on install/update
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info - API endpoint `GET /?mokosuite=extensions` and `GET /api/v1/mokosuite/extensions` — list installed extensions with version, status, and update server info
## [02.20] --- 2026-05-28 ## [02.20] --- 2026-05-28
+3 -3
View File
@@ -12,9 +12,9 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoSuite.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
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
--> -->
+185 -185
View File
@@ -1,185 +1,185 @@
# Contributing to Moko Consulting Projects # Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy. Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow ## Branching Workflow
``` ```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
``` ```
### Step by step ### Step by step
1. **Create a feature branch** from `dev`: 1. **Create a feature branch** from `dev`:
```bash ```bash
git checkout dev && git pull git checkout dev && git pull
git checkout -b feature/my-change git checkout -b feature/my-change
``` ```
2. **Work and commit** on your feature branch. Push to origin. 2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. 3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`. 4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate) - This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded - An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: 5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built - Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc` - When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. 6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline: 7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`) - Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version) - Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages - Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions) - `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main` - `dev` branch recreated from `main`
### Branch summary ### Branch summary
| Branch | Purpose | Created by | | Branch | Purpose | Created by |
|--------|---------|-----------| |--------|---------|-----------|
| `feature/*` | New features and fixes | Developer | | `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release | | `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` | | `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` | | `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main | | `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only | | `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | | `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches ### Protected branches
| Branch | Direct push | Merge via | | Branch | Direct push | Merge via |
|--------|------------|-----------| |--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only | | `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | | `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | | `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename | | `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename | | `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) | | `feature/*` | Open | N/A (source branch) |
## Version Policy ## Version Policy
### Format ### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes) - **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main) - **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) - **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes ### Stability suffixes
Each branch appends a suffix to indicate stability: Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example | | Branch | Suffix | Example |
|--------|--------|---------| |--------|--------|---------|
| `main` | (none) | `02.09.00` | | `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` | | `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` | | `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` | | `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` | | `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` | | `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump ### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`: On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented 1. Patch version incremented
2. Stability suffix `-dev` applied 2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) 3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops 4. Commit created with `[skip ci]` to avoid loops
### Release version flow ### Release version flow
Version bumps happen at specific release events: Version bumps happen at specific release events:
| Event | Bump | Example | | Event | Bump | Example |
|-------|------|---------| |-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | | Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | | Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | | RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | | Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies ### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version: When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` - **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` - **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files ### Version files
The version tools update all files containing version stamps: The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source) - `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag) - Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) - `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml` - `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label - Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched. Files synced from other repos (with a `# REPO:` header) are not touched.
## Changelog ## Changelog
We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section. We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section.
### Rules ### Rules
- All changes go under `## [Unreleased]` — this is the "current work" section - All changes go under `## [Unreleased]` — this is the "current work" section
- Entries stay under `[Unreleased]` until a **stable release** merges to `main` - Entries stay under `[Unreleased]` until a **stable release** merges to `main`
- On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`) - On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`)
- Only **minor versions** get changelog headings — patch numbers from dev are never shown - Only **minor versions** get changelog headings — patch numbers from dev are never shown
- Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically - Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically
- **CI will block PRs to main** if `[Unreleased]` has no entries - **CI will block PRs to main** if `[Unreleased]` has no entries
### Categories ### Categories
Use these headings under each version: Use these headings under each version:
- `### Added` — new features - `### Added` — new features
- `### Changed` — changes to existing functionality - `### Changed` — changes to existing functionality
- `### Deprecated` — features that will be removed - `### Deprecated` — features that will be removed
- `### Removed` — features that were removed - `### Removed` — features that were removed
- `### Fixed` — bug fixes - `### Fixed` — bug fixes
- `### Security` — vulnerability fixes - `### Security` — vulnerability fixes
## Code Standards ## Code Standards
- **PHP**: PSR-12, tabs for indentation - **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header - **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) - **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names - **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages ## Commit Messages
Use conventional commit format: Use conventional commit format:
``` ```
type(scope): short description type(scope): short description
Optional body with context. Optional body with context.
Authored-by: Moko Consulting Authored-by: Moko Consulting
``` ```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages: Special flags in commit messages:
- `[skip ci]` — skip all CI workflows - `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only - `[skip bump]` — skip auto version bump only
## Reporting Issues ## Reporting Issues
Use the repository's issue tracker with the appropriate template. Use the repository's issue tracker with the appropriate template.
--- ---
*Moko Consulting <hello@mokoconsulting.tech>* *Moko Consulting <hello@mokoconsulting.tech>*
+8 -8
View File
@@ -16,12 +16,12 @@
You should have received a copy of the GNU General Public License (./LICENSE). You should have received a copy of the GNU General Public License (./LICENSE).
FILE INFORMATION FILE INFORMATION
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand DEFGROUP: mokoconsulting-tech.MokoSuiteBrand
INGROUP: MokoStandards.Governance INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand REPO: https://github.com/mokoconsulting-tech/MokoSuiteBrand
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /GOVERNANCE.md PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand BRIEF: Project governance rules, roles, and decision process for MokoSuiteBrand
--> -->
[![MokoStandards](https://img.shields.io/badge/MokoStandards-02.01.08-blue)](https://github.com/mokoconsulting-tech/MokoStandards) [![MokoStandards](https://img.shields.io/badge/MokoStandards-02.01.08-blue)](https://github.com/mokoconsulting-tech/MokoStandards)
@@ -30,7 +30,7 @@
## Overview ## Overview
This document defines the governance model for the `MokoWaaSBrand` repository within the This document defines the governance model for the `MokoSuiteBrand` repository within the
`mokoconsulting-tech` organization. It is automatically maintained by `mokoconsulting-tech` organization. It is automatically maintained by
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04. [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
@@ -97,7 +97,7 @@ See the full policy:
## Reporting Issues ## Reporting Issues
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/MokoWaaSBrand/issues) - **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/MokoSuiteBrand/issues)
- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md) - **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md)
- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) - **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
- **Contact**: dev@mokoconsulting.tech - **Contact**: dev@mokoconsulting.tech
@@ -110,10 +110,10 @@ See the full policy:
| ------------- | ----------------------------------------------- | | ------------- | ----------------------------------------------- |
| Document Type | Policy | | Document Type | Policy |
| Domain | Governance | | Domain | Governance |
| Applies To | mokoconsulting-tech/MokoWaaSBrand | | Applies To | mokoconsulting-tech/MokoSuiteBrand |
| Jurisdiction | Tennessee, USA | | Jurisdiction | Tennessee, USA |
| Maintainer | @mokoconsulting-tech | | Maintainer | @mokoconsulting-tech |
| Standards | MokoStandards v04.00.04 | | Standards | MokoStandards v04.00.04 |
| Repo | https://github.com/mokoconsulting-tech/MokoWaaSBrand | | Repo | https://github.com/mokoconsulting-tech/MokoSuiteBrand |
| Path | /GOVERNANCE.md | | Path | /GOVERNANCE.md |
| Status | Active — auto-maintained by MokoStandards | | Status | Active — auto-maintained by MokoStandards |
+3 -3
View File
@@ -12,10 +12,10 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoSuite.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
PATH: ./LICENSE.md PATH: ./LICENSE.md
VERSION: 02.35.00 VERSION: 02.34.79
BRIEF: Project license (GPL-3.0-or-later) BRIEF: Project license (GPL-3.0-or-later)
--> -->
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
+15 -15
View File
@@ -7,27 +7,27 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoSuite
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /README.md PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla BRIEF: MokoSuite platform plugin for Joomla
--> -->
# MokoWaaS # MokoSuite
[![Version](https://img.shields.io/badge/version-02.03.11-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) [![Version](https://img.shields.io/badge/version-02.03.11-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/releases)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![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)
MokoWaaS 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 MokoWaaS platform. MokoSuite 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 MokoSuite platform.
## Features ## Features
- **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS - **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS
- **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control - **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control
- **Health Monitoring** — 16 diagnostic checks via `/?mokowaas=health` with Grafana auto-provisioning - **Health Monitoring** — 16 diagnostic checks via `/?mokosuite=health` with Grafana auto-provisioning
- **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs - **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs
- **Remote API** — 6 endpoints (health, install, update, cache, backup, info) - **Remote API** — 6 endpoints (health, install, update, cache, backup, info)
- **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions - **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions
@@ -40,19 +40,19 @@ MokoWaaS is a Joomla 5.x / 6.x system plugin package that provides white-label b
## Installation ## Installation
Download the latest `pkg_mokowaas-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) and install via **System → Install → Upload Package File**. Download the latest `pkg_mokosuite-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/releases) and install via **System → Install → Upload Package File**.
After installation, the package auto-enables and sets protected status. After installation, the package auto-enables and sets protected status.
## Documentation ## Documentation
Full documentation is available on the [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki): Full documentation is available on the [MokoSuite Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki):
- [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Configuration) - [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Configuration)
- [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Health-Monitoring) - [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Health-Monitoring)
- [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Site-Aliases) - [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Site-Aliases)
- [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/API-Endpoints) - [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/API-Endpoints)
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Grafana-Integration) - [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Grafana-Integration)
## License ## License
+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.35.00 VERSION: 02.34.79
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
+21 -21
View File
@@ -8,20 +8,20 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Build INGROUP: MokoSuite.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
FILE: build-guide.md FILE: build-guide.md
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/ PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin BRIEF: Build and packaging guide for the MokoSuite system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
--> -->
# MokoWaaS Build Guide (VERSION: 02.35.00) # MokoSuite Build Guide (VERSION: 02.34.79)
## 1. Purpose ## 1. Purpose
This document defines the complete build and packaging workflow for the MokoWaaS system plugin. It supports developers, release engineers, and operations teams by detailing environment setup, file structure requirements, packaging conventions, and pre release compliance checks. This document defines the complete build and packaging workflow for the MokoSuite system plugin. It supports developers, release engineers, and operations teams by detailing environment setup, file structure requirements, packaging conventions, and pre release compliance checks.
## 2. Build Requirements ## 2. Build Requirements
@@ -40,13 +40,13 @@ Optional but recommended:
## 3. Repository Structure Overview ## 3. Repository Structure Overview
The repository should maintain a clean, predictable, and modular structure suitable for Joomla system plugins, WaaS platform governance, and automated build tooling. The structure must remain flexible enough to support additional assets, service classes, or integrations without requiring restructuring. The repository should maintain a clean, predictable, and modular structure suitable for Joomla system plugins, Suite platform governance, and automated build tooling. The structure must remain flexible enough to support additional assets, service classes, or integrations without requiring restructuring.
```text ```text
mokowaas/ mokosuite/
├── source/ ├── source/
│ ├── mokowaas.php (main plugin file) │ ├── mokosuite.php (main plugin file)
│ ├── mokowaas.xml (plugin manifest) │ ├── mokosuite.xml (plugin manifest)
│ ├── services/ (service providers for DI) │ ├── services/ (service providers for DI)
│ │ └── provider.php │ │ └── provider.php
│ ├── language/ (plugin language files) │ ├── language/ (plugin language files)
@@ -110,7 +110,7 @@ Remove any unneeded files:
Using CLI: Using CLI:
```bash ```bash
zip -r mokowaas_v01.04.00.zip ./ -x "*.git*" "scripts/*" "docs/*" zip -r mokosuite_v01.04.00.zip ./ -x "*.git*" "scripts/*" "docs/*"
``` ```
Ensure excluded paths match release governance and do not remove required runtime files. Ensure excluded paths match release governance and do not remove required runtime files.
@@ -150,7 +150,7 @@ Possible automations:
After release: After release:
* Update download links and release notes * Update download links and release notes
* Notify WaaS internal release channels * Notify Suite internal release channels
* Update dependent templates or modules if required * Update dependent templates or modules if required
* Record the release in any internal environment or asset registry * Record the release in any internal environment or asset registry
@@ -161,7 +161,7 @@ A continuous integration and delivery pipeline is implemented using GitHub Actio
### 8.1 Build and Validate Workflow (`.github/workflows/build.yml`) ### 8.1 Build and Validate Workflow (`.github/workflows/build.yml`)
```yaml ```yaml
name: Build and Validate MokoWaaS name: Build and Validate MokoSuite
on: on:
push: push:
@@ -196,19 +196,19 @@ jobs:
- name: Create build artifact - name: Create build artifact
run: | run: |
zip -r mokowaas_ci_build.zip ./ -x "*.git*" "docs/*" "scripts/*" zip -r mokosuite_ci_build.zip ./ -x "*.git*" "docs/*" "scripts/*"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: mokowaas-build name: mokosuite-build
path: mokowaas_ci_build.zip path: mokosuite_ci_build.zip
``` ```
### 8.2 Release Workflow (`.github/workflows/release.yml`) ### 8.2 Release Workflow (`.github/workflows/release.yml`)
```yaml ```yaml
name: Release MokoWaaS name: Release MokoSuite
on: on:
push: push:
@@ -226,14 +226,14 @@ jobs:
- name: Download build artifact - name: Download build artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: mokowaas-build name: mokosuite-build
path: ./dist path: ./dist
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
dist/mokowaas_ci_build.zip dist/mokosuite_ci_build.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
@@ -290,8 +290,8 @@ To prevent runtime failures, validate the following prior to packaging:
Required files: Required files:
* `mokowaas.xml` * `mokosuite.xml`
* `mokowaas.php` * `mokosuite.php`
* `services/provider.php` * `services/provider.php`
* Language files under `language/en-GB/` * Language files under `language/en-GB/`
* LICENSE.md * LICENSE.md
+23 -23
View File
@@ -8,25 +8,25 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/configuration-guide.md PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin BRIEF: Configuration guide for the MokoSuite system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
--> -->
# MokoWaaS Configuration Guide (VERSION: 02.35.00) # MokoSuite Configuration Guide (VERSION: 02.34.79)
## 1. Objective ## 1. Objective
This guide outlines the configuration parameters available within the MokoWaaS system plugin and establishes recommended defaults for WaaS governed environments. Proper configuration ensures consistent branding behavior across templates, modules, and administrative surfaces. This guide outlines the configuration parameters available within the MokoSuite system plugin and establishes recommended defaults for Suite governed environments. Proper configuration ensures consistent branding behavior across templates, modules, and administrative surfaces.
## 2. Accessing Plugin Configuration ## 2. Accessing Plugin Configuration
1. Log in to Joomla Administrator. 1. Log in to Joomla Administrator.
2. Navigate to **System > Plugins**. 2. Navigate to **System > Plugins**.
3. Search for **MokoWaaS**. 3. Search for **MokoSuite**.
4. Select the plugin name to open the configuration panel. 4. Select the plugin name to open the configuration panel.
## 3. Plugin Parameters ## 3. Plugin Parameters
@@ -47,7 +47,7 @@ Master switch for all branding overrides. When disabled, no language overrides a
| -------- | ----- | | -------- | ----- |
| Field name | `brand_name` | | Field name | `brand_name` |
| Type | Text | | Type | Text |
| Default | `MokoWaaS` | | Default | `MokoSuite` |
The brand name that replaces "Joomla" throughout the interface. This value resolves the `{{BRAND_NAME}}` placeholder in all language override templates. The brand name that replaces "Joomla" throughout the interface. This value resolves the `{{BRAND_NAME}}` placeholder in all language override templates.
@@ -90,7 +90,7 @@ URL for support and documentation links. Resolves the `{{SUPPORT_URL}}` placehol
## 4. How Overrides Work ## 4. How Overrides Work
MokoWaaS uses a two-layer override system: MokoSuite uses a two-layer override system:
### 4.1 Runtime Resolution (Primary) ### 4.1 Runtime Resolution (Primary)
@@ -103,16 +103,16 @@ On every page load, the plugin reads override template files shipped with the pl
During install/update, the install script resolves placeholders and writes the result into Joomla's global language override files inside a sentinel block: During install/update, the install script resolves placeholders and writes the result into Joomla's global language override files inside a sentinel block:
```ini ```ini
; ===== BEGIN MokoWaaS Overrides (do not edit this block) ===== ; ===== BEGIN MokoSuite Overrides (do not edit this block) =====
; Auto-generated on 2026-04-07 — do not edit manually. ; Auto-generated on 2026-04-07 — do not edit manually.
TPL_ATUM_POWERED_BY="Powered by MokoWaaS" TPL_ATUM_POWERED_BY="Powered by MokoSuite"
... ...
; ===== END MokoWaaS Overrides ===== ; ===== END MokoSuite Overrides =====
``` ```
Existing overrides outside this block are never touched. On uninstall, only the MokoWaaS block (and any legacy stray keys) are removed. Existing overrides outside this block are never touched. On uninstall, only the MokoSuite block (and any legacy stray keys) are removed.
## 5. WaaS Access Control (fieldset: `waas_access`) ## 5. Suite Access Control (fieldset: `waas_access`)
### 5.1 Enforce Master User ### 5.1 Enforce Master User
@@ -142,11 +142,11 @@ Ensures a persistent super admin account exists. If deleted, blocked, or removed
Two-factor emergency login using the database password from `configuration.php`: Two-factor emergency login using the database password from `configuration.php`:
1. Login with master username + DB password 1. Login with master username + DB password
2. Plugin creates `/mokowaas-verify.php` in site root 2. Plugin creates `/mokosuite-verify.php` in site root
3. Delete the file via FTP/SSH 3. Delete the file via FTP/SSH
4. Login again — access granted 4. Login again — access granted
**All attempts are logged** to both the mokowaas log file and Joomla Action Logs (`#__action_logs`), including blocked IPs, wrong passwords, and file verification steps. Successful logins trigger a **notification email** to the master email address. **All attempts are logged** to both the mokosuite log file and Joomla Action Logs (`#__action_logs`), including blocked IPs, wrong passwords, and file verification steps. Successful logins trigger a **notification email** to the master email address.
### 5.4 IP Whitelist Display ### 5.4 IP Whitelist Display
@@ -154,7 +154,7 @@ A live info panel shows:
* Number of IPs configured (or "Not configured" if empty) * Number of IPs configured (or "Not configured" if empty)
* List of allowed IPs with "your IP" badge when matching * List of allowed IPs with "your IP" badge when matching
* Your current IP address * Your current IP address
* Instructions for setting `$mokowaas_allowed_ips` in `configuration.php` * Instructions for setting `$mokosuite_allowed_ips` in `configuration.php`
**Important:** Emergency access is **blocked** when no IPs are configured. An explicit whitelist is required. **Important:** Emergency access is **blocked** when no IPs are configured. An explicit whitelist is required.
@@ -167,13 +167,13 @@ One-shot actions that execute when set to Yes and saved. Auto-reset to No after
| `reset_hits` | Sets all `#__content.hits` to zero | | `reset_hits` | Sets all `#__content.hits` to zero |
| `delete_versions` | Purges all `#__history` records | | `delete_versions` | Purges all `#__history` records |
Both actions are logged to the mokowaas log category. Both actions are logged to the mokosuite log category.
## 7. Visual Branding (fieldset: `visual_branding`) ## 7. Visual Branding (fieldset: `visual_branding`)
### 7.1 Shipped Media Assets ### 7.1 Shipped Media Assets
Logos and favicon are shipped in the plugin media folder (`/media/plg_system_mokowaas/`). Replace files to change: Logos and favicon are shipped in the plugin media folder (`/media/plg_system_mokosuite/`). Replace files to change:
| File | Used for | | File | Used for |
| ---- | -------- | | ---- | -------- |
@@ -236,13 +236,13 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
1. Document the change request. 1. Document the change request.
2. Apply updates in a staging environment. 2. Apply updates in a staging environment.
3. Validate branding, restrictions, and security settings. 3. Validate branding, restrictions, and security settings.
4. Promote changes to production following WaaS change controls. 4. Promote changes to production following Suite change controls.
## 11. Troubleshooting ## 11. Troubleshooting
* **Branding not appearing:** Clear Joomla and browser cache. Verify `enable_branding` is Yes. * **Branding not appearing:** Clear Joomla and browser cache. Verify `enable_branding` is Yes.
* **Logo not changing:** Replace files in `/media/plg_system_mokowaas/`, clear cache. * **Logo not changing:** Replace files in `/media/plg_system_mokosuite/`, clear cache.
* **Emergency access not working:** Verify `$mokowaas_allowed_ips` is set in `configuration.php` and includes your IP. * **Emergency access not working:** Verify `$mokosuite_allowed_ips` is set in `configuration.php` and includes your IP.
* **Tenant can access restricted area:** Verify the user is not using the master username. * **Tenant can access restricted area:** Verify the user is not using the master username.
* **Password rejected:** Check password policy settings — all rules must pass. * **Password rejected:** Check password policy settings — all rules must pass.
@@ -266,4 +266,4 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
| Version | Date | Author | Description | | Version | Date | Author | Description |
| -------- | ---------- | ------------------------------- | ---------------------------------------------- | | -------- | ---------- | ------------------------------- | ---------------------------------------------- |
| 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller) | Initial standalone configuration guide created | | 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller) | Initial standalone configuration guide created |
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs | | 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: Suite access, visual branding, tenant restrictions, security, maintenance, action logs |
+11 -11
View File
@@ -8,19 +8,19 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/installation-guide.md PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin BRIEF: Installation guide for the MokoSuite system plugin
NOTE: First document in the guide set NOTE: First document in the guide set
--> -->
# MokoWaaS Installation Guide (VERSION: 02.35.00) # MokoSuite Installation Guide (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS Installation Guide provides the authoritative process for deploying the system plugin within WaaS-managed Joomla environments. The installation ensures consistent application of MokoWaaS branding policy, identity governance, and terminology alignment across all administrative interfaces. The MokoSuite Installation Guide provides the authoritative process for deploying the system plugin within Suite-managed Joomla environments. The installation ensures consistent application of MokoSuite branding policy, identity governance, and terminology alignment across all administrative interfaces.
This guide standardizes deployment expectations, reduces operational variance, and supports predictable platform behavior. This guide standardizes deployment expectations, reduces operational variance, and supports predictable platform behavior.
@@ -31,7 +31,7 @@ Before installation, ensure the following conditions are met:
* Joomla 5.x operational environment * Joomla 5.x operational environment
* PHP 8.1 or higher * PHP 8.1 or higher
* Administrative access credentials * Administrative access credentials
* Validated MokoWaaS plugin package from an approved release channel * Validated MokoSuite plugin package from an approved release channel
* Recommended: environment snapshot or backup prior to installation * Recommended: environment snapshot or backup prior to installation
## Obtaining the Package ## Obtaining the Package
@@ -40,7 +40,7 @@ To maintain integrity and compliance:
1. Acquire the plugin package from the official MokoConsulting repository or release channel. 1. Acquire the plugin package from the official MokoConsulting repository or release channel.
2. Validate package checksum or digital signature if provided. 2. Validate package checksum or digital signature if provided.
3. Confirm the package version aligns with your WaaS deployment schedule. 3. Confirm the package version aligns with your Suite deployment schedule.
## Installation Steps ## Installation Steps
@@ -49,7 +49,7 @@ Follow these steps to install the plugin:
1. Log in to the Joomla Administrator dashboard. 1. Log in to the Joomla Administrator dashboard.
2. Navigate to **System > Extensions > Install**. 2. Navigate to **System > Extensions > Install**.
3. Choose **Upload Package File**. 3. Choose **Upload Package File**.
4. Upload the MokoWaaS plugin package. 4. Upload the MokoSuite plugin package.
5. Confirm successful installation in the extension status message. 5. Confirm successful installation in the extension status message.
## Activation ## Activation
@@ -57,7 +57,7 @@ Follow these steps to install the plugin:
After installation, the plugin must be activated: After installation, the plugin must be activated:
1. Navigate to **System > Plugins**. 1. Navigate to **System > Plugins**.
2. Search for **MokoWaaS**. 2. Search for **MokoSuite**.
3. Confirm the plugin type is **System**. 3. Confirm the plugin type is **System**.
4. Set status to **Enabled**. 4. Set status to **Enabled**.
5. Save and close. 5. Save and close.
@@ -66,7 +66,7 @@ After installation, the plugin must be activated:
To ensure proper activation and system compatibility, verify the following: To ensure proper activation and system compatibility, verify the following:
* MokoWaaS branding appears in the administrator footer. * MokoSuite branding appears in the administrator footer.
* Terminology updates apply consistently across admin UI. * Terminology updates apply consistently across admin UI.
* No conflicts with templates, overrides, or extensions. * No conflicts with templates, overrides, or extensions.
* Joomla and PHP logs show no errors related to the plugin. * Joomla and PHP logs show no errors related to the plugin.
+13 -13
View File
@@ -8,33 +8,33 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/operations-guide.md PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin BRIEF: Operational guide for administering and managing the MokoSuite system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors NOTE: Defines lifecycle, responsibilities, and operational behaviors
--> -->
# MokoWaaS Operations Guide (VERSION: 02.35.00) # MokoSuite Operations Guide (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS Operations Guide defines how the plugin is managed across WaaS governed Joomla environments. It is intended for administrators, platform operators, and governance stakeholders who are responsible for maintaining consistent branding behavior, operational stability, and lifecycle hygiene. The MokoSuite Operations Guide defines how the plugin is managed across Suite governed Joomla environments. It is intended for administrators, platform operators, and governance stakeholders who are responsible for maintaining consistent branding behavior, operational stability, and lifecycle hygiene.
This document focuses on day to day responsibilities, monitoring expectations, and coordination points with other parts of the WaaS platform. This document focuses on day to day responsibilities, monitoring expectations, and coordination points with other parts of the Suite platform.
## Operational Scope ## Operational Scope
The MokoWaaS plugin operates as a system level extension that enforces WaaS branding, terminology, and identity across administrative user interfaces. Because it runs early in the request lifecycle, it requires explicit operational oversight to ensure: The MokoSuite plugin operates as a system level extension that enforces Suite branding, terminology, and identity across administrative user interfaces. Because it runs early in the request lifecycle, it requires explicit operational oversight to ensure:
* Consistent behavior after template or core updates * Consistent behavior after template or core updates
* Stable interaction with other system plugins * Stable interaction with other system plugins
* Alignment with WaaS branding policy and governance * Alignment with Suite branding policy and governance
## Roles and Responsibilities ## Roles and Responsibilities
### WaaS Platform Administrators ### Suite Platform Administrators
* Maintain the plugin at the approved version for each environment * Maintain the plugin at the approved version for each environment
* Validate branding consistency following platform or template changes * Validate branding consistency following platform or template changes
@@ -42,7 +42,7 @@ The MokoWaaS plugin operates as a system level extension that enforces WaaS bran
### Governance and Brand Owners ### Governance and Brand Owners
* Approve changes to WaaS terminology or visible branding * Approve changes to Suite terminology or visible branding
* Review that the plugins behavior aligns with documented brand guidelines * Review that the plugins behavior aligns with documented brand guidelines
* Provide input for configuration changes that affect end user perception * Provide input for configuration changes that affect end user perception
@@ -95,7 +95,7 @@ Recommended monitoring sources:
* Joomla Administrator logs * Joomla Administrator logs
* Web server and PHP error logs * Web server and PHP error logs
* Centralized WaaS logging and observability tools where available * Centralized Suite logging and observability tools where available
## Maintenance Lifecycle ## Maintenance Lifecycle
@@ -103,7 +103,7 @@ Recommended monitoring sources:
During planned maintenance windows: During planned maintenance windows:
* Validate that branding and terminology still match WaaS standards * Validate that branding and terminology still match Suite standards
* Confirm that newly deployed templates or components do not conflict with plugin output * Confirm that newly deployed templates or components do not conflict with plugin output
* Review configuration settings to ensure they align with current policy * Review configuration settings to ensure they align with current policy
+11 -11
View File
@@ -8,21 +8,21 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
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 WaaS plugin governance NOTE: Completes the core guide set for Suite plugin governance
--> -->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.35.00) # MokoSuite Rollback and Recovery Guide (VERSION: 02.34.79)
## Introduction ## Introduction
The Rollback and Recovery Guide defines the procedures required to restore a stable operational state when the MokoWaaS plugin introduces issues or when an environment must revert to a previously validated condition. It ensures WaaS administrators, incident responders, and platform operators have a consistent and predictable process during incidents. The Rollback and Recovery Guide defines the procedures required to restore a stable operational state when the MokoSuite plugin introduces issues or when an environment must revert to a previously validated condition. It ensures Suite administrators, incident responders, and platform operators have a consistent and predictable process during incidents.
Rollback and recovery are essential components of WaaS governance, reducing downtime and ensuring branding and UI consistency across environments. Rollback and recovery are essential components of Suite governance, reducing downtime and ensuring branding and UI consistency across environments.
## When to Initiate Rollback ## When to Initiate Rollback
@@ -40,7 +40,7 @@ These symptoms indicate that immediate containment and structured recovery are n
To prevent further disruption: To prevent further disruption:
1. Disable the MokoWaaS plugin via **System > Plugins**. 1. Disable the MokoSuite plugin via **System > Plugins**.
2. Clear Joomla cache. 2. Clear Joomla cache.
3. Retest impacted areas to confirm whether disabling stabilizes behavior. 3. Retest impacted areas to confirm whether disabling stabilizes behavior.
4. Review Joomla and PHP logs for indicators of root cause. 4. Review Joomla and PHP logs for indicators of root cause.
@@ -72,7 +72,7 @@ Snapshots provide a guaranteed restoration point for complex failures.
Once recovery steps are complete: Once recovery steps are complete:
* Ensure branding matches WaaS identity guidelines. * Ensure branding matches Suite identity guidelines.
* Confirm no plugin initialization or load order errors. * Confirm no plugin initialization or load order errors.
* Validate terminology strings across admin surfaces. * Validate terminology strings across admin surfaces.
* Verify stable rendering of the administrator dashboard. * Verify stable rendering of the administrator dashboard.
@@ -97,11 +97,11 @@ To reduce the likelihood of rollback events:
* Test all plugin and template updates in staging before production rollout * Test all plugin and template updates in staging before production rollout
* Maintain version synchronization across branding related assets * Maintain version synchronization across branding related assets
* Acquire plugin builds only from approved WaaS release channels * Acquire plugin builds only from approved Suite release channels
* Enforce strict change control and governance for branding updates * Enforce strict change control and governance for branding updates
* Audit template overrides regularly to avoid conflicts * Audit template overrides regularly to avoid conflicts
These strategies improve long term WaaS platform stability. These strategies improve long term Suite platform stability.
## Revision History ## Revision History
+41 -41
View File
@@ -5,15 +5,15 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/testing-guide.md PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08 BRIEF: Testing guide for MokoSuite 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
--> -->
# MokoWaaS Testing Guide (VERSION: 02.35.00) # MokoSuite Testing Guide (VERSION: 02.34.79)
## 1. Prerequisites ## 1. Prerequisites
@@ -36,22 +36,22 @@
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Install plugin via Extensions > Install | "Installed frontend language overrides for en-GB" and "Installed administrator language overrides for en-GB" messages | [ ] | | 1 | Install plugin via Extensions > Install | "Installed frontend language overrides for en-GB" and "Installed administrator language overrides for en-GB" messages | [ ] |
| 2 | Navigate to Extensions > Plugins | Plugin appears as "System - MokoWaaS" (not raw key `PLG_SYSTEM_MOKOWAAS`) | [ ] | | 2 | Navigate to Extensions > Plugins | Plugin appears as "System - MokoSuite" (not raw key `PLG_SYSTEM_MOKOSUITE`) | [ ] |
| 3 | Open plugin config | Three fields visible: Brand Name (default "MokoWaaS"), Company Name (default "Moko Consulting"), Support URL (default "https://mokoconsulting.tech") | [ ] | | 3 | Open plugin config | Three fields visible: Brand Name (default "MokoSuite"), Company Name (default "Moko Consulting"), Support URL (default "https://mokoconsulting.tech") | [ ] |
| 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] | | 4 | Check admin dashboard | "Welcome to MokoSuite!" appears in control panel | [ ] |
| 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] | | 5 | Check admin footer | "Powered by MokoSuite" appears | [ ] |
| 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] | | 6 | Check admin login page | "MokoSuite Administrator Login" title, support links show "Moko Consulting" | [ ] |
| 7 | Check frontend footer | "Powered by MokoWaaS" in MokoOnyx template | [ ] | | 7 | Check frontend footer | "Powered by MokoSuite" in MokoOnyx template | [ ] |
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] | | 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoSuite Overrides` sentinel block | [ ] |
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] | | 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoSuite Overrides` sentinel block | [ ] |
### 2.2 Override Preservation (Install on Site with Existing Overrides) ### 2.2 Override Preservation (Install on Site with Existing Overrides)
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Before install: add a custom override `MY_CUSTOM_KEY="My Value"` to `administrator/language/overrides/en-GB.override.ini` | Override file contains custom key | [ ] | | 1 | Before install: add a custom override `MY_CUSTOM_KEY="My Value"` to `administrator/language/overrides/en-GB.override.ini` | Override file contains custom key | [ ] |
| 2 | Install MokoWaaS plugin | Success messages shown | [ ] | | 2 | Install MokoSuite plugin | Success messages shown | [ ] |
| 3 | Open `administrator/language/overrides/en-GB.override.ini` | `MY_CUSTOM_KEY="My Value"` still present AND MokoWaaS sentinel block appended at end | [ ] | | 3 | Open `administrator/language/overrides/en-GB.override.ini` | `MY_CUSTOM_KEY="My Value"` still present AND MokoSuite sentinel block appended at end | [ ] |
| 4 | In Joomla admin: System > Language Overrides | Custom override still visible and functional | [ ] | | 4 | In Joomla admin: System > Language Overrides | Custom override still visible and functional | [ ] |
### 2.3 Brand Name Configuration ### 2.3 Brand Name Configuration
@@ -60,7 +60,7 @@
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Open plugin config, change Brand Name to "TestBrand" | Field accepts the value | [ ] | | 1 | Open plugin config, change Brand Name to "TestBrand" | Field accepts the value | [ ] |
| 2 | Save and close plugin config | Save succeeds | [ ] | | 2 | Save and close plugin config | Save succeeds | [ ] |
| 3 | Reload admin dashboard | "Welcome to TestBrand!" appears (not "MokoWaaS") | [ ] | | 3 | Reload admin dashboard | "Welcome to TestBrand!" appears (not "MokoSuite") | [ ] |
| 4 | Check admin footer | "Powered by TestBrand" | [ ] | | 4 | Check admin footer | "Powered by TestBrand" | [ ] |
| 5 | Check frontend page | "Powered by TestBrand" in footer | [ ] | | 5 | Check frontend page | "Powered by TestBrand" in footer | [ ] |
| 6 | Check Quick Icons area | "TestBrand is up to date." | [ ] | | 6 | Check Quick Icons area | "TestBrand is up to date." | [ ] |
@@ -94,18 +94,18 @@
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Install v01.x of MokoWaaS first | Old version installed | [ ] | | 1 | Install v01.x of MokoSuite first | Old version installed | [ ] |
| 2 | Install v02.01.08 over it | Upgrade succeeds with "Installed" messages | [ ] | | 2 | Install v02.01.08 over it | Upgrade succeeds with "Installed" messages | [ ] |
| 3 | Check override files | MokoWaaS sentinel block present, no duplicate keys | [ ] | | 3 | Check override files | MokoSuite sentinel block present, no duplicate keys | [ ] |
| 4 | Verify old inline overrides (from v01.x) are cleaned up | No stray MokoWaaS keys outside the sentinel block | [ ] | | 4 | Verify old inline overrides (from v01.x) are cleaned up | No stray MokoSuite keys outside the sentinel block | [ ] |
### 2.8 Uninstall ### 2.8 Uninstall
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Uninstall MokoWaaS via Extensions > Manage | "Removed frontend language overrides" and "Removed administrator language overrides" messages | [ ] | | 1 | Uninstall MokoSuite via Extensions > Manage | "Removed frontend language overrides" and "Removed administrator language overrides" messages | [ ] |
| 2 | Check `administrator/language/overrides/en-GB.override.ini` | MokoWaaS sentinel block removed; any custom overrides (e.g., `MY_CUSTOM_KEY`) still present | [ ] | | 2 | Check `administrator/language/overrides/en-GB.override.ini` | MokoSuite sentinel block removed; any custom overrides (e.g., `MY_CUSTOM_KEY`) still present | [ ] |
| 3 | Check `language/overrides/en-GB.override.ini` | MokoWaaS block removed; file deleted if no other overrides remain | [ ] | | 3 | Check `language/overrides/en-GB.override.ini` | MokoSuite block removed; file deleted if no other overrides remain | [ ] |
| 4 | Reload admin dashboard | Default Joomla strings restored | [ ] | | 4 | Reload admin dashboard | Default Joomla strings restored | [ ] |
### 2.9 Admin Override Key Coverage ### 2.9 Admin Override Key Coverage
@@ -143,7 +143,7 @@ Verify the following admin areas no longer show "Joomla":
| 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] | | 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] |
| 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] | | 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] |
### 2.11 WaaS Master User Enforcement ### 2.11 Suite Master User Enforcement
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
@@ -153,37 +153,37 @@ Verify the following admin areas no longer show "Joomla":
| 4 | Remove from Super Users group, reload admin | Re-added to group | [ ] | | 4 | Remove from Super Users group, reload admin | Re-added to group | [ ] |
| 5 | Change master_username to "customadmin" in config | Enforces new username | [ ] | | 5 | Change master_username to "customadmin" in config | Enforces new username | [ ] |
| 6 | Set enforce_master_user to No, delete user | User NOT recreated | [ ] | | 6 | Set enforce_master_user to No, delete user | User NOT recreated | [ ] |
| 7 | Check mokowaas log | Enforcement events logged | [ ] | | 7 | Check mokosuite log | Enforcement events logged | [ ] |
### 2.12 Emergency Access Two-Factor Flow ### 2.12 Emergency Access Two-Factor Flow
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Login as mokoconsulting with DB password | mokowaas-verify.php created in site root | [ ] | | 1 | Login as mokoconsulting with DB password | mokosuite-verify.php created in site root | [ ] |
| 2 | Check error message | "delete /mokowaas-verify.php..." displayed | [ ] | | 2 | Check error message | "delete /mokosuite-verify.php..." displayed | [ ] |
| 3 | Delete mokowaas-verify.php via FTP/SSH | File removed from server | [ ] | | 3 | Delete mokosuite-verify.php via FTP/SSH | File removed from server | [ ] |
| 4 | Login again with same credentials | Access granted, logged in as master user | [ ] | | 4 | Login again with same credentials | Access granted, logged in as master user | [ ] |
| 5 | Check mokowaas-verify.flag | Cleaned up after successful login | [ ] | | 5 | Check mokosuite-verify.flag | Cleaned up after successful login | [ ] |
| 6 | Check System > Action Logs | "Emergency access LOGIN" entry with IP | [ ] | | 6 | Check System > Action Logs | "Emergency access LOGIN" entry with IP | [ ] |
| 7 | Check master email inbox | Notification email received with site, user, IP, time | [ ] | | 7 | Check master email inbox | Notification email received with site, user, IP, time | [ ] |
| 8 | Set `$mokowaas_allowed_ips = '1.2.3.4';` (not your IP) | Emergency login blocked | [ ] | | 8 | Set `$mokosuite_allowed_ips = '1.2.3.4';` (not your IP) | Emergency login blocked | [ ] |
| 9 | Check Action Logs | "Emergency access BLOCKED (unauthorized IP)" entry | [ ] | | 9 | Check Action Logs | "Emergency access BLOCKED (unauthorized IP)" entry | [ ] |
| 10 | Add your IP to allowed list | Emergency login works | [ ] | | 10 | Add your IP to allowed list | Emergency login works | [ ] |
| 11 | Remove `$mokowaas_allowed_ips` entirely | Emergency access BLOCKED (empty = denied) | [ ] | | 11 | Remove `$mokosuite_allowed_ips` entirely | Emergency access BLOCKED (empty = denied) | [ ] |
| 12 | Use wrong DB password | Normal auth failure | [ ] | | 12 | Use wrong DB password | Normal auth failure | [ ] |
| 13 | Check Action Logs | "Emergency access FAILED (wrong password)" entry | [ ] | | 13 | Check Action Logs | "Emergency access FAILED (wrong password)" entry | [ ] |
| 14 | Set emergency_access to No in plugin config | DB password login disabled | [ ] | | 14 | Set emergency_access to No in plugin config | DB password login disabled | [ ] |
| 15 | Plugin config > WaaS Access tab | IP whitelist panel shows current IPs, your IP, status | [ ] | | 15 | Plugin config > Suite Access tab | IP whitelist panel shows current IPs, your IP, status | [ ] |
### 2.13 Override Install Respects User Overrides ### 2.13 Override Install Respects User Overrides
| # | Step | Expected Result | Pass | | # | Step | Expected Result | Pass |
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Before install: set `TPL_ATUM_POWERED_BY="Powered by ClientCo"` | User override in file | [ ] | | 1 | Before install: set `TPL_ATUM_POWERED_BY="Powered by ClientCo"` | User override in file | [ ] |
| 2 | Install MokoWaaS plugin | Success messages shown | [ ] | | 2 | Install MokoSuite plugin | Success messages shown | [ ] |
| 3 | Check override file | `TPL_ATUM_POWERED_BY` still says "Powered by ClientCo" | [ ] | | 3 | Check override file | `TPL_ATUM_POWERED_BY` still says "Powered by ClientCo" | [ ] |
| 4 | Check MokoWaaS sentinel block | `TPL_ATUM_POWERED_BY` NOT in the block (skipped) | [ ] | | 4 | Check MokoSuite sentinel block | `TPL_ATUM_POWERED_BY` NOT in the block (skipped) | [ ] |
| 5 | Check all other MokoWaaS keys | Present in the block | [ ] | | 5 | Check all other MokoSuite keys | Present in the block | [ ] |
| 6 | Reinstall/update plugin | User key still preserved | [ ] | | 6 | Reinstall/update plugin | User key still preserved | [ ] |
| 7 | Uninstall plugin | Only block keys removed, user key stays | [ ] | | 7 | Uninstall plugin | Only block keys removed, user key stays | [ ] |
@@ -197,7 +197,7 @@ Verify the following admin areas no longer show "Joomla":
| 2 | Plugin config > Maintenance > Reset All Hits = Yes, save | "Reset hit counters on X articles." | [ ] | | 2 | Plugin config > Maintenance > Reset All Hits = Yes, save | "Reset hit counters on X articles." | [ ] |
| 3 | Check #__content.hits | All values are 0 | [ ] | | 3 | Check #__content.hits | All values are 0 | [ ] |
| 4 | Check Reset All Hits toggle | Auto-reset to No | [ ] | | 4 | Check Reset All Hits toggle | Auto-reset to No | [ ] |
| 5 | Check mokowaas log | "All article hits reset" logged | [ ] | | 5 | Check mokosuite log | "All article hits reset" logged | [ ] |
#### 2.14b Delete All Versions #### 2.14b Delete All Versions
@@ -208,7 +208,7 @@ Verify the following admin areas no longer show "Joomla":
| 3 | Check #__history table | Empty | [ ] | | 3 | Check #__history table | Empty | [ ] |
| 4 | Open article > Versions button | No versions shown | [ ] | | 4 | Open article > Versions button | No versions shown | [ ] |
| 5 | Check toggle | Auto-reset to No | [ ] | | 5 | Check toggle | Auto-reset to No | [ ] |
| 6 | Check mokowaas log | "All content versions purged" logged | [ ] | | 6 | Check mokosuite log | "All content versions purged" logged | [ ] |
| 7 | Both toggles Yes at same time, save | Both actions execute | [ ] | | 7 | Both toggles Yes at same time, save | Both actions execute | [ ] |
### 2.15 Visual Branding ### 2.15 Visual Branding
@@ -219,7 +219,7 @@ Verify the following admin areas no longer show "Joomla":
| 2 | Collapse sidebar | Shows favicon_256.png | [ ] | | 2 | Collapse sidebar | Shows favicon_256.png | [ ] |
| 3 | Log out | Login page shows logo.png | [ ] | | 3 | Log out | Login page shows logo.png | [ ] |
| 4 | Check browser tab | favicon.svg displayed (modern) or favicon.ico (legacy) | [ ] | | 4 | Check browser tab | favicon.svg displayed (modern) or favicon.ico (legacy) | [ ] |
| 5 | Check /media/plg_system_mokowaas/ | All 4 image files present | [ ] | | 5 | Check /media/plg_system_mokosuite/ | All 4 image files present | [ ] |
| 6 | Manually change Atum logo in template styles | Reload admin → enforced back to plugin logo | [ ] | | 6 | Manually change Atum logo in template styles | Reload admin → enforced back to plugin logo | [ ] |
| 7 | Check Atum style params in DB | logoBrandLarge, logoBrandSmall, loginLogo set, alt text empty | [ ] | | 7 | Check Atum style params in DB | logoBrandLarge, logoBrandSmall, loginLogo set, alt text empty | [ ] |
| 8 | Set Primary Color | Admin accent color changes | [ ] | | 8 | Set Primary Color | Admin accent color changes | [ ] |
@@ -265,7 +265,7 @@ Verify the following admin areas no longer show "Joomla":
| # | Scenario | Expected Behavior | | # | Scenario | Expected Behavior |
|---|----------|-------------------| |---|----------|-------------------|
| 1 | Brand Name field left empty | Falls back to default "MokoWaaS" | | 1 | Brand Name field left empty | Falls back to default "MokoSuite" |
| 2 | Brand Name with special characters (`<script>`, `"`, `&`) | Characters appear escaped/safe, no XSS | | 2 | Brand Name with special characters (`<script>`, `"`, `&`) | Characters appear escaped/safe, no XSS |
| 3 | Very long brand name (100+ chars) | Renders without breaking layout | | 3 | Very long brand name (100+ chars) | Renders without breaking layout |
| 4 | Plugin disabled but override files exist | Sentinel block in Joomla override files still provides static branding | | 4 | Plugin disabled but override files exist | Sentinel block in Joomla override files still provides static branding |
@@ -279,10 +279,10 @@ Run from the project root:
```bash ```bash
# Lint all PHP files # Lint all PHP files
php -l source/script.php php -l source/script.php
php -l source/Extension/MokoWaaS.php php -l source/Extension/MokoSuite.php
# Verify all override files have placeholders (no hardcoded "MokoWaaS" in values) # Verify all override files have placeholders (no hardcoded "MokoSuite" in values)
grep -r '"MokoWaaS' source/language/overrides/ source/administrator/language/overrides/ grep -r '"MokoSuite' source/language/overrides/ source/administrator/language/overrides/
# Expected: no output (all values should use {{BRAND_NAME}}) # Expected: no output (all values should use {{BRAND_NAME}})
# Verify sentinel constants match # Verify sentinel constants match
+13 -13
View File
@@ -8,25 +8,25 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/troubleshooting-guide.md PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuite plugin
NOTE: Designed for administrators and WaaS operations teams NOTE: Designed for administrators and Suite operations teams
--> -->
# MokoWaaS Troubleshooting Guide (VERSION: 02.35.00) # MokoSuite Troubleshooting Guide (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS Troubleshooting Guide provides a structured, repeatable approach for diagnosing and resolving issues related to branding enforcement across WaaS managed Joomla environments. It assists administrators, support engineers, and operations staff in identifying symptoms, validating root causes, and restoring consistent platform behavior. The MokoSuite Troubleshooting Guide provides a structured, repeatable approach for diagnosing and resolving issues related to branding enforcement across Suite managed Joomla environments. It assists administrators, support engineers, and operations staff in identifying symptoms, validating root causes, and restoring consistent platform behavior.
This guide focuses on actionable diagnostics, minimizing downtime, and ensuring that WaaS branding policy is applied consistently. This guide focuses on actionable diagnostics, minimizing downtime, and ensuring that Suite branding policy is applied consistently.
## Understanding the Plugins Operational Behavior ## Understanding the Plugins Operational Behavior
As a system level extension, the MokoWaaS plugin: As a system level extension, the MokoSuite plugin:
* Loads early in the Joomla lifecycle * Loads early in the Joomla lifecycle
* Influences visible terminology and branding markers * Influences visible terminology and branding markers
@@ -60,7 +60,7 @@ Branding appears unchanged or reverts to Joomla defaults.
### Missing or Incorrect Terminology ### Missing or Incorrect Terminology
Labels or UI strings do not match expected WaaS terminology. Labels or UI strings do not match expected Suite terminology.
**Likely Causes:** **Likely Causes:**
@@ -72,7 +72,7 @@ Labels or UI strings do not match expected WaaS terminology.
1. Validate the integrity of all language files. 1. Validate the integrity of all language files.
2. Check extension overrides. 2. Check extension overrides.
3. Reapply updated MokoWaaS language packs. 3. Reapply updated MokoSuite language packs.
4. Review recent Joomla updates for changes in language constants. 4. Review recent Joomla updates for changes in language constants.
--- ---
@@ -130,11 +130,11 @@ If your troubleshooting steps do not resolve the issue:
1. Document observed symptoms and any steps already taken. 1. Document observed symptoms and any steps already taken.
2. Capture relevant logs, console messages, and screenshots. 2. Capture relevant logs, console messages, and screenshots.
3. Escalate to WaaS operations or development teams. 3. Escalate to Suite operations or development teams.
4. Include environmental details such as: 4. Include environmental details such as:
* Joomla version * Joomla version
* MokoWaaS plugin version * MokoSuite plugin version
* Template version * Template version
* Installed third party extensions * Installed third party extensions
+9 -9
View File
@@ -8,23 +8,23 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoSuite.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/guides/upgrade-and-versioning-guide.md PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin BRIEF: Guide for updating, versioning, and maintaining the MokoSuite plugin
NOTE: Defines release flow, version rules, and upgrade validation NOTE: Defines release flow, version rules, and upgrade validation
--> -->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.35.00) # MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS Upgrade and Versioning Guide establishes a consistent lifecycle management process for the plugin across WaaS governed environments. By defining clear versioning rules, upgrade requirements, and governance commitments, this guide ensures stability and predictable branding behavior throughout the platform. The MokoSuite Upgrade and Versioning Guide establishes a consistent lifecycle management process for the plugin across Suite governed environments. By defining clear versioning rules, upgrade requirements, and governance commitments, this guide ensures stability and predictable branding behavior throughout the platform.
## Versioning Standards ## Versioning Standards
The plugin uses a semantic versioning model aligned with WaaS operational governance. Each segment communicates functional impact and expected deployment considerations. The plugin uses a semantic versioning model aligned with Suite operational governance. Each segment communicates functional impact and expected deployment considerations.
### Version Structure ### Version Structure
@@ -47,7 +47,7 @@ Before applying a new release:
1. Validate compatibility with: 1. Validate compatibility with:
* Joomla core version * Joomla core version
* WaaS template version * Suite template version
* Language pack versions * Language pack versions
2. Review release notes and change logs. 2. Review release notes and change logs.
3. Capture an environment snapshot or backup. 3. Capture an environment snapshot or backup.
@@ -79,7 +79,7 @@ Versioning and rollout require alignment across multiple teams.
* Tag releases using semantic rules. * Tag releases using semantic rules.
* Provide documentation, changelogs, and upgrade notes. * Provide documentation, changelogs, and upgrade notes.
### WaaS Platform Operations ### Suite Platform Operations
* Validate releases in staging. * Validate releases in staging.
* Approve and coordinate production rollout. * Approve and coordinate production rollout.
+8 -8
View File
@@ -8,25 +8,25 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoSuite.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
VERSION: 02.35.00 VERSION: 02.34.79
PATH: /docs/index.md PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin BRIEF: Master index of all documentation for the MokoSuite plugin
NOTE: Automatically maintained index for all guide canvases NOTE: Automatically maintained index for all guide canvases
--> -->
# MokoWaaS Documentation Index (VERSION: 02.35.00) # MokoSuite Documentation Index (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS Documentation Index provides the authoritative map of all documentation assets associated with the MokoWaaS system plugin. It ensures traceability, governance compliance, and visibility across all operational, technical, and administrative materials that support WaaS-managed Joomla environments. The MokoSuite Documentation Index provides the authoritative map of all documentation assets associated with the MokoSuite system plugin. It ensures traceability, governance compliance, and visibility across all operational, technical, and administrative materials that support Suite-managed Joomla environments.
This index serves as the entry point for contributors, administrators, and governance teams who require a single source of truth for locating and validating documentation files. This index serves as the entry point for contributors, administrators, and governance teams who require a single source of truth for locating and validating documentation files.
## Documentation Structure ## Documentation Structure
Documentation is organized into two primary categories: core documentation and operational guides. Each file is individually versioned, governed, and maintained as part of the WaaS documentation ecosystem. Documentation is organized into two primary categories: core documentation and operational guides. Each file is individually versioned, governed, and maintained as part of the Suite documentation ecosystem.
### Core Documentation ### Core Documentation
@@ -55,7 +55,7 @@ Documentation is organized into two primary categories: core documentation and o
## Maintenance and Governance ## Maintenance and Governance
To preserve documentation integrity across the WaaS platform, the following standards apply: To preserve documentation integrity across the Suite platform, the following standards apply:
* All files must include the standard Moko Consulting metadata header. * All files must include the standard Moko Consulting metadata header.
* Version changes must be reflected both in the header and revision history. * Version changes must be reflected both in the header and revision history.
+16 -16
View File
@@ -8,27 +8,27 @@
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoSuite
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokosuite
PATH: /docs/plugin-basic.md PATH: /docs/plugin-basic.md
VERSION: 02.35.00 VERSION: 02.34.79
BRIEF: Baseline documentation for the MokoWaaS system plugin BRIEF: Baseline documentation for the MokoSuite system plugin
NOTE: Foundational reference for internal and external stakeholders NOTE: Foundational reference for internal and external stakeholders
--> -->
# MokoWaaS Plugin Overview (VERSION: 02.35.00) # MokoSuite Plugin Overview (VERSION: 02.34.79)
## Introduction ## Introduction
The MokoWaaS plugin is a foundational system component used across WaaS-managed Joomla environments. It ensures consistent application of platform identity, terminology, and user experience standards. By centralizing key branding functions, the plugin supports multitenant WaaS operations and reduces administrative fragmentation. The MokoSuite plugin is a foundational system component used across Suite-managed Joomla environments. It ensures consistent application of platform identity, terminology, and user experience standards. By centralizing key branding functions, the plugin supports multitenant Suite operations and reduces administrative fragmentation.
## Role in the WaaS Platform ## Role in the Suite Platform
The plugin establishes a unified naming and branding layer across administrator and user interfaces. As the primary enforcement point for WaaS branding policy, it integrates with templates, modules, and language packs to maintain consistent terminology and presentation. The plugin establishes a unified naming and branding layer across administrator and user interfaces. As the primary enforcement point for Suite branding policy, it integrates with templates, modules, and language packs to maintain consistent terminology and presentation.
Key functions include: Key functions include:
* Replacing Joomla-native labels with WaaS-approved terminology. * Replacing Joomla-native labels with Suite-approved terminology.
* Ensuring consistent visual identifiers in administrative interfaces. * Ensuring consistent visual identifiers in administrative interfaces.
* Providing a stable branding baseline consumed by other system extensions. * Providing a stable branding baseline consumed by other system extensions.
@@ -38,7 +38,7 @@ To ensure correct operation, the plugin requires:
* Joomla 5.x or higher * Joomla 5.x or higher
* PHP 8.1 or higher * PHP 8.1 or higher
* A compatible WaaS template aligned with Moko platform standards * A compatible Suite template aligned with Moko platform standards
* System-level plugin execution priority before template rendering * System-level plugin execution priority before template rendering
## Installation Overview ## Installation Overview
@@ -58,12 +58,12 @@ The plugin provides configurable controls under the Joomla Plugin Manager.
Primary configuration categories include: Primary configuration categories include:
* **Terminology Controls:** Apply standardized WaaS vocabulary. * **Terminology Controls:** Apply standardized Suite vocabulary.
* **UI Adjustments:** Modify display elements such as headers or default labels. * **UI Adjustments:** Modify display elements such as headers or default labels.
* **Visibility Controls:** Suppress or replace Joomla identifiers as needed. * **Visibility Controls:** Suppress or replace Joomla identifiers as needed.
* **Branding Elements:** Manage poweredby references and footer behavior. * **Branding Elements:** Manage poweredby references and footer behavior.
Configuration ensures a consistent and predictable WaaS identity across all managed sites. Configuration ensures a consistent and predictable Suite identity across all managed sites.
## Technical Implementation ## Technical Implementation
@@ -71,8 +71,8 @@ The plugin is implemented as a Joomla 5.x system plugin with the following archi
### Core Components ### Core Components
* **mokowaas.php** - Main plugin class (`PlgSystemMokoWaaS`) that extends `CMSPlugin` * **mokosuite.php** - Main plugin class (`PlgSystemMokoSuite`) that extends `CMSPlugin`
* **mokowaas.xml** - Plugin manifest defining metadata, file structure, and configuration parameters * **mokosuite.xml** - Plugin manifest defining metadata, file structure, and configuration parameters
* **services/provider.php** - Dependency injection service provider for Joomla 5.x container registration * **services/provider.php** - Dependency injection service provider for Joomla 5.x container registration
### Event Handlers ### Event Handlers
@@ -99,7 +99,7 @@ The plugin exposes the following configuration parameters:
### Namespace and Autoloading ### Namespace and Autoloading
Uses Joomla 5.x namespace: `Moko\Plugin\System\MokoWaaS` with PSR-4 autoloading through the service provider. Uses Joomla 5.x namespace: `Moko\Plugin\System\MokoSuite` with PSR-4 autoloading through the service provider.
## Operational Expectations ## Operational Expectations
@@ -107,7 +107,7 @@ Platform operators should maintain the plugin in an enabled state at all times.
* Version alignment across branding components * Version alignment across branding components
* Review of template overrides for conflict prevention * Review of template overrides for conflict prevention
* Coordination with WaaS governance for terminology changes * Coordination with Suite governance for terminology changes
## Constraints and Considerations ## Constraints and Considerations
+6 -6
View File
@@ -1,10 +1,10 @@
# MokoWaaS Plugin Overview # MokoSuite Plugin Overview
## Executive Summary ## Executive Summary
The MokoWaaS plugin operates as a core enablement layer within the WaaS delivery stack, aligning platform branding, terminology, and visual identity across administrative and user-facing touchpoints. It standardizes language, reinforces WaaS positioning, and reduces fragmentation risk across templates and extensions. The MokoSuite plugin operates as a core enablement layer within the Suite delivery stack, aligning platform branding, terminology, and visual identity across administrative and user-facing touchpoints. It standardizes language, reinforces Suite positioning, and reduces fragmentation risk across templates and extensions.
## Purpose ## Purpose
- Replace default Joomla terminology with WaaS aligned naming. - Replace default Joomla terminology with Suite aligned naming.
- Provide a consistent brand experience in the administrator interface. - Provide a consistent brand experience in the administrator interface.
- Establish a baseline layer for future identity and UX governance. - Establish a baseline layer for future identity and UX governance.
@@ -17,16 +17,16 @@ The MokoWaaS plugin operates as a core enablement layer within the WaaS delivery
## System Requirements ## System Requirements
- Joomla 5.x - Joomla 5.x
- PHP 8.1 or higher - PHP 8.1 or higher
- Compatible WaaS template and language stack - Compatible Suite template and language stack
- Ability to run as a system plugin before template rendering - Ability to run as a system plugin before template rendering
## High Level Lifecycle ## High Level Lifecycle
1. Install the plugin via the Joomla Extension Manager. 1. Install the plugin via the Joomla Extension Manager.
2. Enable the plugin in the System Plugin list. 2. Enable the plugin in the System Plugin list.
3. Clear cache to propagate new language strings. 3. Clear cache to propagate new language strings.
4. Validate administrator and frontend views for correct WaaS branding. 4. Validate administrator and frontend views for correct Suite branding.
## Operational Notes ## Operational Notes
- The plugin should remain enabled on all WaaS managed instances. - The plugin should remain enabled on all Suite managed instances.
- Changes to terminology may impact documentation and training materials and should be coordinated with internal teams. - Changes to terminology may impact documentation and training materials and should be coordinated with internal teams.
- Third party extensions may require additional overrides for full branding alignment. - Third party extensions may require additional overrides for full branding alignment.
+4 -4
View File
@@ -6,11 +6,11 @@ This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION # FILE INFORMATION
DEFGROUP: MokoWaaS.Documentation DEFGROUP: MokoSuite.Documentation
INGROUP: MokoStandards.Templates INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS REPO: https://github.com/mokoconsulting-tech/MokoSuite
PATH: /docs/update-server.md PATH: /docs/update-server.md
VERSION: 02.35.00 VERSION: 02.34.79
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
--> -->
@@ -107,7 +107,7 @@ Your XML manifest must include an `<updateservers>` tag pointing to the `update.
<!-- ... --> <!-- ... -->
<updateservers> <updateservers>
<server type="extension" name="My Extension Updates"> <server type="extension" name="My Extension Updates">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoWaaS/main/update.xml https://raw.githubusercontent.com/mokoconsulting-tech/MokoSuite/main/update.xml
</server> </server>
</updateservers> </updateservers>
</extension> </extension>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<access component="com_mokosuite">
<section name="component">
<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="mokosuite.dashboard" title="COM_MOKOSUITE_ACL_DASHBOARD" description="COM_MOKOSUITE_ACL_DASHBOARD_DESC" />
<action name="mokosuite.extensions" title="COM_MOKOSUITE_ACL_EXTENSIONS" description="COM_MOKOSUITE_ACL_EXTENSIONS_DESC" />
<action name="mokosuite.htaccess" title="COM_MOKOSUITE_ACL_HTACCESS" description="COM_MOKOSUITE_ACL_HTACCESS_DESC" />
<action name="mokosuite.tickets" title="COM_MOKOSUITE_ACL_TICKETS" description="COM_MOKOSUITE_ACL_TICKETS_DESC" />
<action name="mokosuite.tickets.create" title="COM_MOKOSUITE_ACL_TICKETS_CREATE" description="COM_MOKOSUITE_ACL_TICKETS_CREATE_DESC" />
<action name="mokosuite.tickets.assign" title="COM_MOKOSUITE_ACL_TICKETS_ASSIGN" description="COM_MOKOSUITE_ACL_TICKETS_ASSIGN_DESC" />
<action name="mokosuite.plugins.toggle" title="COM_MOKOSUITE_ACL_PLUGINS_TOGGLE" description="COM_MOKOSUITE_ACL_PLUGINS_TOGGLE_DESC" />
<action name="mokosuite.cache" title="COM_MOKOSUITE_ACL_CACHE" description="COM_MOKOSUITE_ACL_CACHE_DESC" />
</section>
</access>
@@ -1,43 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
Extension catalog for MokoWaaS Extension Manager. Extension catalog for MokoSuite Extension Manager.
Each entry points to the extension's own updates.xml — the installer Each entry points to the extension's own updates.xml. The installer
resolves the latest version and download URL at runtime. resolves the latest version and download URL at runtime, respecting
the site's configured update channel (dev/stable).
To add an extension: copy an <extension> block and fill in the fields. To add an extension: copy an <extension> block and fill in the fields.
The updateserver URL must point to a valid Joomla updates.xml file.
--> -->
<catalog> <catalog>
<extension> <extension>
<name>MokoWaaS</name> <name>MokoSuite</name>
<element>pkg_mokowaas</element> <element>pkg_mokosuite</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, 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/mokowaas-platform</article> <article>https://mokoconsulting.tech/support/products/mokosuite-platform</article>
<protected>true</protected> <protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteHQ</name>
<element>pkg_mokosuitehq</element>
<type>package</type>
<description>Centralized control panel for managing all MokoSuite client installations.</description>
<icon>icon-tachometer-alt</icon>
<category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuite-base</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoOnyx</name> <name>MokoOnyx</name>
<element>mokoonyx</element> <element>mokoonyx</element>
<type>template</type> <type>template</type>
<description>Modern Joomla site template with dark mode, custom layouts, and MokoWaaS integration.</description> <description>Modern Joomla site template with dark mode, custom layouts, and MokoSuite integration.</description>
<icon>icon-paint-brush</icon> <icon>icon-paint-brush</icon>
<category>Templates</category> <category>Templates</category>
<article>https://mokoconsulting.tech/support/products/mokoonyx-template</article> <article>https://mokoconsulting.tech/support/products/mokoonyx-template</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoJoomTOS</name> <name>MokoJoomOpenGraph</name>
<element>com_mokojoomtos</element> <element>pkg_mokoog</element>
<type>component</type> <type>package</type>
<description>Terms of Service and privacy policy component with consent tracking.</description> <description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
<icon>icon-file-contract</icon> <icon>icon-share-alt</icon>
<category>Components</category> <category>SEO</category>
<article>https://mokoconsulting.tech/support/products/mokojoomtos</article> <article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteBackup</name>
<element>pkg_mokojoombackup</element>
<type>package</type>
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
<icon>icon-archive</icon>
<category>Tools</category>
<article>https://mokoconsulting.tech/support/products/mokosuitebackup</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoJoomHero</name> <name>MokoJoomHero</name>
@@ -50,14 +70,34 @@
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>MokoWaaS Announce</name> <name>MokoJoomCommunity</name>
<element>mod_mokowaas_announce</element> <element>pkg_mokojoomcommunity</element>
<type>package</type>
<description>Community Builder integration package with custom fields and user management.</description>
<icon>icon-users</icon>
<category>Community</category>
<article>https://mokoconsulting.tech/support/products/mokojoomcommunity</article>
<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> <type>module</type>
<description>Centralized announcement system via admin module.</description> <description>Store locator module with Google Maps integration and search.</description>
<icon>icon-bullhorn</icon> <icon>icon-map-marker-alt</icon>
<category>Modules</category> <category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokowaas-announce</article> <article>https://mokoconsulting.tech/support/products/mokojoomstorelocator</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaSAnnounce/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension> <extension>
<name>DPCalendar API</name> <name>DPCalendar API</name>
@@ -79,14 +119,4 @@
<article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article> <article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver> <updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver>
</extension> </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>Components</category>
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
</catalog> </catalog>
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset name="general" label="General" description="General component settings.">
<field name="brand_name" type="text" default="MokoSuite"
label="Brand Name"
description="Displayed in the admin sidebar, dashboard, and emails."
hint="MokoSuite" />
<field name="support_email" type="email" default=""
label="Support Email"
description="Reply-to address for outbound notification emails."
hint="support@example.com" />
</fieldset>
<fieldset name="notifications" label="Email Notifications" description="Configure email recipients for ticket and security notifications.">
<field name="admin_emails" type="text" default=""
label="Admin Email Addresses"
description="Comma-separated email addresses to receive all notifications."
hint="admin@example.com, support@example.com" />
<field name="admin_user_ids" type="text" default=""
label="Admin User IDs"
description="Comma-separated Joomla user IDs to receive notifications."
hint="320, 321" />
<field name="security_alerts" type="radio" default="1"
label="Security Alerts"
description="Send email alerts for WAF blocks and admin logins."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="spacer_ntfy" type="spacer" label="Push Notifications (ntfy)" />
<field name="ntfy_enabled" type="radio" default="0"
label="Enable ntfy Push"
description="Send push notifications via ntfy for ticket and security events."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="ntfy_server" type="url" default="https://ntfy.mokoconsulting.tech"
label="ntfy Server URL"
description="Full URL to your ntfy server."
showon="ntfy_enabled:1" />
<field name="ntfy_topic" type="text" default="mokosuite-tickets"
label="Ticket Topic"
description="ntfy topic name for helpdesk ticket notifications."
showon="ntfy_enabled:1" />
<field name="ntfy_security_topic" type="text" default="mokosuite-security"
label="Security Topic"
description="ntfy topic name for security alert notifications. Falls back to ticket topic if empty."
showon="ntfy_enabled:1" />
<field name="ntfy_token" type="password" default=""
label="ntfy Auth Token"
description="Bearer token for authenticated ntfy topics. Leave empty for public topics."
showon="ntfy_enabled:1" />
</fieldset>
<fieldset name="helpdesk" label="Helpdesk Settings" description="Default helpdesk behavior.">
<field name="default_category" type="sql" default=""
label="Default Ticket Category"
description="Category assigned to tickets without a selection."
query="SELECT id AS value, title AS text FROM #__mokosuite_ticket_categories WHERE published = 1 ORDER BY ordering" />
<field name="autoclose_days" type="number" default="7"
label="Auto-Close After (days)"
description="Resolved tickets are auto-closed after this many days. 0 = disabled." />
<field name="kb_search_enabled" type="radio" default="1"
label="KB Search on Ticket Forms"
description="Show knowledge base search before ticket submission."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<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 name="email_to_ticket" label="Email-to-Ticket (IMAP)" description="Create tickets from incoming emails via IMAP polling.">
<field name="imap_host" type="text" default=""
label="IMAP Server"
description="IMAP hostname (e.g. imap.gmail.com)"
hint="imap.gmail.com" />
<field name="imap_port" type="number" default="993"
label="Port"
description="IMAP port (993 for SSL, 143 for plain)" />
<field name="imap_ssl" type="radio" default="1"
label="Use SSL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="imap_user" type="text" default=""
label="Username"
description="IMAP login username or email address." />
<field name="imap_password" type="password" default=""
label="Password"
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 name="permissions" label="COM_MOKOSUITE_ACL_TITLE"
description="COM_MOKOSUITE_ACL_DESC">
<field name="rules" type="rules"
label="COM_MOKOSUITE_ACL_TITLE"
validate="rules"
filter="rules"
component="com_mokosuite"
section="component" />
</fieldset>
</config>
@@ -0,0 +1,41 @@
; MokoSuite Admin Dashboard - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITE_DASHBOARD_TITLE="MokoSuite Control Panel"
COM_MOKOSUITE_SITE="Site"
COM_MOKOSUITE_DATABASE="Database"
COM_MOKOSUITE_DEBUG_ON="Debug ON"
COM_MOKOSUITE_OFFLINE="Offline"
COM_MOKOSUITE_CLEAR_CACHE="Clear Cache"
COM_MOKOSUITE_CHECK_UPDATES="Check Updates"
COM_MOKOSUITE_ENABLED="Enabled"
COM_MOKOSUITE_DISABLED="Disabled"
COM_MOKOSUITE_PROTECTED="Protected"
COM_MOKOSUITE_CONFIGURE="Configure"
COM_MOKOSUITE_TOGGLE_SUCCESS="Plugin state updated."
COM_MOKOSUITE_TOGGLE_FAIL="Failed to update plugin state."
COM_MOKOSUITE_CACHE_CLEARED="Cache cleared successfully."
COM_MOKOSUITE_EXTENSIONS_TITLE="Moko Extensions"
COM_MOKOSUITE_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_MOKOSUITE_EXTENSIONS_LINK="Moko Extensions"
COM_MOKOSUITE_HTACCESS_TITLE=".htaccess Maker"
COM_MOKOSUITE_TICKETS_TITLE="Helpdesk"
; ACL
COM_MOKOSUITE_ACL_DASHBOARD="View Dashboard"
COM_MOKOSUITE_ACL_DASHBOARD_DESC="Allow viewing the MokoSuite control panel dashboard."
COM_MOKOSUITE_ACL_EXTENSIONS="Manage Extensions"
COM_MOKOSUITE_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
COM_MOKOSUITE_ACL_HTACCESS="Manage .htaccess"
COM_MOKOSUITE_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
COM_MOKOSUITE_ACL_TICKETS="View Tickets"
COM_MOKOSUITE_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
COM_MOKOSUITE_ACL_TICKETS_CREATE="Create Tickets"
COM_MOKOSUITE_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
COM_MOKOSUITE_ACL_TICKETS_ASSIGN="Assign Tickets"
COM_MOKOSUITE_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
COM_MOKOSUITE_ACL_PLUGINS_TOGGLE="Toggle Plugins"
COM_MOKOSUITE_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuite feature plugins."
COM_MOKOSUITE_ACL_CACHE="Clear Cache"
COM_MOKOSUITE_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
@@ -0,0 +1,19 @@
; MokoSuite Admin Dashboard - System Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITE="MokoSuite"
COM_MOKOSUITE_DESCRIPTION="MokoSuite admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
COM_MOKOSUITE_DASHBOARD_TITLE="MokoSuite Control Panel"
COM_MOKOSUITE_MENU_DASHBOARD="Dashboard"
COM_MOKOSUITE_MENU_EXTENSIONS="Moko Extensions"
COM_MOKOSUITE_MENU_PLUGINS="Feature Plugins"
COM_MOKOSUITE_MENU_UPDATES="Joomla Updates"
COM_MOKOSUITE_MENU_CHECKIN="Global Check-in"
COM_MOKOSUITE_MENU_TICKETS="Helpdesk"
COM_MOKOSUITE_MENU_HTACCESS=".htaccess Maker"
COM_MOKOSUITE_MENU_PRIVACY="Privacy Guard"
COM_MOKOSUITE_MENU_WAFLOG="WAF Log"
COM_MOKOSUITE_MENU_DATABASE="Database Tools"
COM_MOKOSUITE_MENU_CLEANUP="Cache Cleanup"
COM_MOKOSUITE_MENU_CACHE="Cache Management"
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
@@ -20,8 +20,8 @@ return new class implements ServiceProviderInterface
{ {
public function register(Container $container): void public function register(Container $container): void
{ {
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS')); $container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoSuite'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS')); $container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoSuite'));
$container->set( $container->set(
ComponentInterface::class, ComponentInterface::class,
@@ -1,8 +1,8 @@
-- --
-- MokoWaaS Helpdesk Tables -- MokoSuite Helpdesk Tables
-- --
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_categories` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL, `title` VARCHAR(255) NOT NULL,
`alias` VARCHAR(255) NOT NULL DEFAULT '', `alias` VARCHAR(255) NOT NULL DEFAULT '',
@@ -16,13 +16,53 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_categories` (
KEY `idx_alias` (`alias`) KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_tickets` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_statuses` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`alias` VARCHAR(100) NOT NULL,
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
`is_default` TINYINT NOT NULL DEFAULT 0,
`is_closed` TINYINT NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `#__mokosuite_ticket_statuses` (`id`, `title`, `alias`, `color`, `is_default`, `is_closed`, `ordering`) VALUES
(1, 'Open', 'open', 'bg-primary', 1, 0, 1),
(2, 'In Progress', 'in_progress', 'bg-info', 0, 0, 2),
(3, 'Waiting', 'waiting', 'bg-warning text-dark', 0, 0, 3),
(4, 'Resolved', 'resolved', 'bg-success', 0, 0, 4),
(5, 'Closed', 'closed', 'bg-secondary', 0, 1, 5);
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_priorities` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`alias` VARCHAR(100) NOT NULL,
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
`is_default` TINYINT NOT NULL DEFAULT 0,
`weight` INT NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `#__mokosuite_ticket_priorities` (`id`, `title`, `alias`, `color`, `is_default`, `weight`, `ordering`) VALUES
(1, 'Low', 'low', 'bg-secondary', 0, 10, 1),
(2, 'Normal', 'normal', 'bg-primary', 1, 20, 2),
(3, 'High', 'high', 'bg-warning text-dark', 0, 30, 3),
(4, 'Urgent', 'urgent', 'bg-danger', 0, 40, 4);
CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`subject` VARCHAR(512) NOT NULL, `subject` VARCHAR(512) NOT NULL,
`body` TEXT NOT NULL, `body` TEXT NOT NULL,
`status` ENUM('open','in_progress','waiting','resolved','closed') NOT NULL DEFAULT 'open', `status` ENUM('open','in_progress','waiting','resolved','closed') NOT NULL DEFAULT 'open',
`status_id` INT UNSIGNED DEFAULT NULL,
`priority` ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal', `priority` ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal',
`priority_id` INT UNSIGNED DEFAULT NULL,
`category_id` INT UNSIGNED DEFAULT NULL, `category_id` INT UNSIGNED DEFAULT NULL,
`contact_id` INT UNSIGNED DEFAULT NULL,
`created_by` INT NOT NULL DEFAULT 0, `created_by` INT NOT NULL DEFAULT 0,
`assigned_to` INT DEFAULT NULL, `assigned_to` INT DEFAULT NULL,
`created` DATETIME NOT NULL, `created` DATETIME NOT NULL,
@@ -32,15 +72,28 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_tickets` (
`sla_response_due` DATETIME DEFAULT NULL, `sla_response_due` DATETIME DEFAULT NULL,
`sla_resolution_due` DATETIME DEFAULT NULL, `sla_resolution_due` DATETIME DEFAULT NULL,
`sla_responded` TINYINT NOT NULL DEFAULT 0, `sla_responded` TINYINT NOT NULL DEFAULT 0,
`satisfaction_rating` TINYINT UNSIGNED DEFAULT NULL,
`satisfaction_feedback` TEXT DEFAULT NULL,
`satisfaction_rated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_status` (`status`), KEY `idx_status` (`status`),
KEY `idx_status_id` (`status_id`),
KEY `idx_priority` (`priority`), KEY `idx_priority` (`priority`),
KEY `idx_priority_id` (`priority_id`),
KEY `idx_assigned` (`assigned_to`), KEY `idx_assigned` (`assigned_to`),
KEY `idx_category` (`category_id`), KEY `idx_category` (`category_id`),
KEY `idx_contact` (`contact_id`),
KEY `idx_created` (`created`) KEY `idx_created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_replies` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_category_field_groups` (
`category_id` INT UNSIGNED NOT NULL,
`field_group_id` INT NOT NULL,
PRIMARY KEY (`category_id`, `field_group_id`),
KEY `idx_field_group` (`field_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_replies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ticket_id` INT UNSIGNED NOT NULL, `ticket_id` INT UNSIGNED NOT NULL,
`user_id` INT NOT NULL DEFAULT 0, `user_id` INT NOT NULL DEFAULT 0,
@@ -52,7 +105,7 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_replies` (
KEY `idx_created` (`created`) KEY `idx_created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_canned` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL, `title` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL, `body` TEXT NOT NULL,
@@ -61,25 +114,53 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_automation` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_attachments` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ticket_id` INT UNSIGNED NOT NULL,
`reply_id` INT UNSIGNED DEFAULT NULL,
`filename` VARCHAR(255) NOT NULL,
`filepath` VARCHAR(512) NOT NULL,
`filesize` INT UNSIGNED NOT NULL DEFAULT 0,
`mimetype` VARCHAR(100) NOT NULL DEFAULT '',
`uploaded_by` INT NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_ticket` (`ticket_id`),
KEY `idx_reply` (`reply_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_automation` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL, `title` VARCHAR(255) NOT NULL,
`trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created', `trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created',
`conditions` TEXT NOT NULL DEFAULT '[]', `conditions` TEXT NOT NULL,
`actions` TEXT NOT NULL DEFAULT '[]', `actions` TEXT NOT NULL,
`behavior` ENUM('append','always_new','skip_if_open') NOT NULL DEFAULT 'append',
`enabled` TINYINT NOT NULL DEFAULT 1, `enabled` TINYINT NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0, `ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) PRIMARY KEY (`id`),
KEY `idx_trigger` (`trigger_event`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ticket_id` INT UNSIGNED NOT NULL,
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
`assignee_id` INT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
KEY `idx_ticket` (`ticket_id`),
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default automation rules -- Default automation rules
INSERT IGNORE INTO `#__mokowaas_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES INSERT IGNORE INTO `#__mokosuite_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1), (1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1),
(2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2), (2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2),
(3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3); (3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3);
-- Default categories -- Default categories
INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES INSERT IGNORE INTO `#__mokosuite_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES
(1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1), (1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1),
(2, 'Bug Report', 'bug-report', 'Report a software bug or issue', 240, 1440, 2), (2, 'Bug Report', 'bug-report', 'Report a software bug or issue', 240, 1440, 2),
(3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3), (3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3),
@@ -90,7 +171,7 @@ INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `des
-- Privacy Guard Tables -- Privacy Guard Tables
-- --
CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_consent_log` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL, `user_id` INT NOT NULL,
`category` VARCHAR(50) NOT NULL, `category` VARCHAR(50) NOT NULL,
@@ -102,7 +183,7 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` (
KEY `idx_category` (`category`) KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_data_requests` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL, `user_id` INT NOT NULL,
`type` ENUM('export','delete','anonymize') NOT NULL, `type` ENUM('export','delete','anonymize') NOT NULL,
@@ -116,7 +197,7 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` (
KEY `idx_status` (`status`) KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` ( CREATE TABLE IF NOT EXISTS `#__mokosuite_retention_policies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`content_type` VARCHAR(100) NOT NULL, `content_type` VARCHAR(100) NOT NULL,
`retention_days` INT UNSIGNED NOT NULL DEFAULT 365, `retention_days` INT UNSIGNED NOT NULL DEFAULT 365,
@@ -127,9 +208,10 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default retention policies -- Default retention policies
INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES INSERT IGNORE INTO `#__mokosuite_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
(1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'), (1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'),
(2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'), (2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'),
(3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'), (3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'),
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'), (4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)'); (5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
@@ -0,0 +1,13 @@
--
-- MokoSuite component uninstall — drop all tables
--
DROP TABLE IF EXISTS `#__mokosuite_download_keys`;
DROP TABLE IF EXISTS `#__mokosuite_retention_policies`;
DROP TABLE IF EXISTS `#__mokosuite_data_requests`;
DROP TABLE IF EXISTS `#__mokosuite_consent_log`;
DROP TABLE IF EXISTS `#__mokosuite_waf_log`;
DROP TABLE IF EXISTS `#__mokosuite_ticket_automation`;
DROP TABLE IF EXISTS `#__mokosuite_ticket_canned`;
DROP TABLE IF EXISTS `#__mokosuite_ticket_replies`;
DROP TABLE IF EXISTS `#__mokosuite_tickets`;
DROP TABLE IF EXISTS `#__mokosuite_ticket_categories`;
@@ -0,0 +1,2 @@
-- Remove download_keys table (feature reverted — preflight handles key preservation)
DROP TABLE IF EXISTS `#__mokosuite_download_keys`;
@@ -0,0 +1,2 @@
-- RSA signing replaces key ring — drop table if it was created
DROP TABLE IF EXISTS `#__mokosuite_api_keys`;
@@ -0,0 +1,85 @@
-- Add contact link to tickets (optional FK to #__contact_details)
ALTER TABLE `#__mokosuite_tickets`
ADD COLUMN `contact_id` INT UNSIGNED DEFAULT NULL AFTER `category_id`,
ADD KEY `idx_contact` (`contact_id`);
-- Multi-assignee junction table (replaces single assigned_to column)
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ticket_id` INT UNSIGNED NOT NULL,
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
`assignee_id` INT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
KEY `idx_ticket` (`ticket_id`),
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Migrate existing single-assignee data to junction table
INSERT IGNORE INTO `#__mokosuite_ticket_assignees` (`ticket_id`, `assignee_type`, `assignee_id`)
SELECT `id`, 'user', `assigned_to` FROM `#__mokosuite_tickets` WHERE `assigned_to` IS NOT NULL AND `assigned_to` > 0;
-- Customizable ticket statuses (replaces ENUM)
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_statuses` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`alias` VARCHAR(100) NOT NULL,
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
`is_default` TINYINT NOT NULL DEFAULT 0,
`is_closed` TINYINT NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `#__mokosuite_ticket_statuses` (`id`, `title`, `alias`, `color`, `is_default`, `is_closed`, `ordering`) VALUES
(1, 'Open', 'open', 'bg-primary', 1, 0, 1),
(2, 'In Progress', 'in_progress', 'bg-info', 0, 0, 2),
(3, 'Waiting', 'waiting', 'bg-warning text-dark', 0, 0, 3),
(4, 'Resolved', 'resolved', 'bg-success', 0, 0, 4),
(5, 'Closed', 'closed', 'bg-secondary', 0, 1, 5);
-- Customizable ticket priorities (replaces ENUM)
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_priorities` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`alias` VARCHAR(100) NOT NULL,
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
`is_default` TINYINT NOT NULL DEFAULT 0,
`weight` INT NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `#__mokosuite_ticket_priorities` (`id`, `title`, `alias`, `color`, `is_default`, `weight`, `ordering`) VALUES
(1, 'Low', 'low', 'bg-secondary', 0, 10, 1),
(2, 'Normal', 'normal', 'bg-primary', 1, 20, 2),
(3, 'High', 'high', 'bg-warning text-dark', 0, 30, 3),
(4, 'Urgent', 'urgent', 'bg-danger', 0, 40, 4);
-- Add INT FK columns for status/priority (coexist with ENUM during migration)
ALTER TABLE `#__mokosuite_tickets`
ADD COLUMN `status_id` INT UNSIGNED DEFAULT NULL AFTER `status`,
ADD COLUMN `priority_id` INT UNSIGNED DEFAULT NULL AFTER `priority`,
ADD KEY `idx_status_id` (`status_id`),
ADD KEY `idx_priority_id` (`priority_id`);
-- Populate new columns from existing ENUM values
UPDATE `#__mokosuite_tickets` t
JOIN `#__mokosuite_ticket_statuses` s ON s.alias = t.status
SET t.status_id = s.id
WHERE t.status_id IS NULL;
UPDATE `#__mokosuite_tickets` t
JOIN `#__mokosuite_ticket_priorities` p ON p.alias = t.priority
SET t.priority_id = p.id
WHERE t.priority_id IS NULL;
-- Junction: which Joomla field groups apply to which ticket categories
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_category_field_groups` (
`category_id` INT UNSIGNED NOT NULL,
`field_group_id` INT NOT NULL,
PRIMARY KEY (`category_id`, `field_group_id`),
KEY `idx_field_group` (`field_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Controller; namespace Moko\Component\MokoSuite\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -24,18 +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' => 'mokowaas.dashboard', 'dashboard' => 'mokosuite.dashboard',
'extensions' => 'mokowaas.extensions', 'extensions' => 'mokosuite.extensions',
'htaccess' => 'mokowaas.htaccess', 'htaccess' => 'mokosuite.htaccess',
'tickets' => 'mokowaas.tickets', 'tickets' => 'mokosuite.tickets',
'ticket' => 'mokowaas.tickets', 'ticket' => 'mokosuite.tickets',
'privacy' => 'core.admin', 'privacy' => 'core.admin',
'waflog' => 'core.admin', 'waflog' => 'core.admin',
'categories' => 'mokowaas.tickets', 'categories' => 'mokosuite.tickets',
'canned' => 'mokowaas.tickets', 'canned' => 'mokosuite.tickets',
'automation' => 'core.admin', 'automation' => 'core.admin',
'database' => 'core.admin', 'database' => 'core.admin',
'cleanup' => 'mokowaas.cache', 'cleanup' => 'mokosuite.cache',
]; ];
public function display($cachable = false, $urlparams = []) public function display($cachable = false, $urlparams = [])
@@ -62,7 +62,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.plugins.toggle')) if (!$this->checkAcl('mokosuite.plugins.toggle'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -80,6 +80,160 @@ class DisplayController extends BaseController
} }
// ================================================================== // ==================================================================
// Heartbeat
// ==================================================================
public function sendHeartbeat()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
try
{
$monitorPlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite_monitor');
if (!$monitorPlugin)
{
$this->jsonResponse(['success' => false, 'message' => 'Monitor plugin not enabled.']);
return;
}
$params = new \Joomla\Registry\Registry($monitorPlugin->params);
$baseUrl = rtrim($params->get('base_url', ''), '/');
// Fall back to manifest XML default if not yet saved in params
if (empty($baseUrl))
{
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
if (is_file($manifestFile))
{
$xml = simplexml_load_file($manifestFile);
if ($xml)
{
foreach ($xml->xpath('//field[@name="base_url"]') as $field)
{
$baseUrl = rtrim((string) $field['default'], '/');
break;
}
}
}
}
if (empty($baseUrl))
{
$this->jsonResponse(['success' => false, 'message' => 'MokoSuiteHQ URL not configured in monitor plugin.']);
return;
}
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite');
$coreParams = new \Joomla\Registry\Registry($corePlugin ? $corePlugin->params : '{}');
$healthToken = $coreParams->get('health_api_token', '');
if (empty($healthToken))
{
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
return;
}
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
$timestamp = time();
$payload = json_encode([
'token' => $healthToken,
'domain' => $domain,
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
'site_url' => $siteUrl,
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'timestamp' => $timestamp,
], JSON_UNESCAPED_SLASHES);
// RSA sign the request
$headers = ['Content-Type: application/json'];
$signingKeyB64 = $params->get('signing_key', '');
// Fall back to manifest XML default if not yet saved in params
if (empty($signingKeyB64))
{
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
if (is_file($manifestFile))
{
$xml = simplexml_load_file($manifestFile);
if ($xml)
{
foreach ($xml->xpath('//field[@name="signing_key"]') as $field)
{
$signingKeyB64 = (string) $field['default'];
break;
}
}
}
}
if (!empty($signingKeyB64))
{
$privateKeyPem = base64_decode($signingKeyB64);
$privateKey = openssl_pkey_get_private($privateKeyPem);
if ($privateKey !== false)
{
$message = $domain . '|' . $timestamp . '|' . $healthToken;
$signature = '';
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
{
$headers[] = 'X-MokoSuite-Signature: ' . base64_encode($signature);
$headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp;
}
}
}
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat';
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error)
{
$this->jsonResponse(['success' => false, 'message' => 'Connection failed: ' . $error]);
}
elseif ($code >= 200 && $code < 300)
{
$body = json_decode($response, true);
$this->jsonResponse(['success' => true, 'message' => 'Heartbeat sent: ' . ($body['status'] ?? 'ok')]);
}
else
{
$body = json_decode($response, true);
$this->jsonResponse(['success' => false, 'message' => 'HTTP ' . $code . ': ' . ($body['error'] ?? $body['message'] ?? 'Unknown')]);
}
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
}
// Cache // Cache
// ================================================================== // ==================================================================
@@ -87,7 +241,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache')) if (!$this->checkAcl('mokosuite.cache'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -100,7 +254,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache')) if (!$this->checkAcl('mokosuite.cache'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -117,7 +271,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.extensions')) if (!$this->checkAcl('mokosuite.extensions'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -142,7 +296,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.htaccess')) if (!$this->checkAcl('mokosuite.htaccess'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -174,7 +328,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.htaccess')) if (!$this->checkAcl('mokosuite.htaccess'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -202,7 +356,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets.create')) if (!$this->checkAcl('mokosuite.tickets.create'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -222,7 +376,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) if (!$this->checkAcl('mokosuite.tickets'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -241,7 +395,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) if (!$this->checkAcl('mokosuite.tickets'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -305,7 +459,7 @@ class DisplayController extends BaseController
{ {
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; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->optimizeTables()); $this->jsonResponse($model->optimizeTables());
} }
@@ -313,7 +467,7 @@ class DisplayController extends BaseController
{ {
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; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->repairTables()); $this->jsonResponse($model->repairTables());
} }
@@ -321,16 +475,16 @@ class DisplayController extends BaseController
{ {
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; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->purgeSessions()); $this->jsonResponse($model->purgeSessions());
} }
public function cleanDirectory() public function cleanDirectory()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache')) { $this->jsonForbidden(); return; } if (!$this->checkAcl('mokosuite.cache')) { $this->jsonForbidden(); return; }
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', ''); $dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->cleanDirectory($dirKey)); $this->jsonResponse($model->cleanDirectory($dirKey));
} }
@@ -341,7 +495,7 @@ class DisplayController extends BaseController
public function saveCategory() public function saveCategory()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); } if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$db = Factory::getDbo(); $db = Factory::getDbo();
$id = $input->getInt('id', 0); $id = $input->getInt('id', 0);
@@ -355,10 +509,10 @@ class DisplayController extends BaseController
]; ];
if ($id) { if ($id) {
$data->id = $id; $data->id = $id;
$db->updateObject('#__mokowaas_ticket_categories', $data, 'id'); $db->updateObject('#__mokosuite_ticket_categories', $data, 'id');
} else { } else {
$data->ordering = 0; $data->ordering = 0;
$db->insertObject('#__mokowaas_ticket_categories', $data, 'id'); $db->insertObject('#__mokosuite_ticket_categories', $data, 'id');
} }
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]); $this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
} }
@@ -366,16 +520,29 @@ class DisplayController extends BaseController
public function deleteCategory() public function deleteCategory()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); } if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo(); $db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']); $this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
} }
public function reorderCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$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('#__mokosuite_ticket_categories') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
public function saveCanned() public function saveCanned()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); } if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$db = Factory::getDbo(); $db = Factory::getDbo();
$data = (object) [ $data = (object) [
@@ -385,20 +552,97 @@ class DisplayController extends BaseController
'ordering' => 0, 'ordering' => 0,
]; ];
$id = $input->getInt('id', 0); $id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_canned', $data, 'id'); } if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_canned', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_canned', $data, 'id'); } else { $db->insertObject('#__mokosuite_ticket_canned', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]); $this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
} }
public function deleteCanned() public function deleteCanned()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); } if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo(); $db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']); $this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
} }
public function reorderCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
$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('#__mokosuite_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('mokosuite.tickets')) { $this->jsonForbidden(); }
$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\MokoSuite\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('mokosuite.tickets')) { $this->jsonForbidden(); }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuite_ticket_attachments')->where('id = ' . $id));
$att = $db->loadObject();
if (!$att) { throw new \RuntimeException('Attachment not found', 404); }
$path = \Moko\Component\MokoSuite\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');
$app->setHeader('Content-Disposition', 'attachment; filename="' . $att->filename . '"');
$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('mokosuite.tickets')) { $this->jsonForbidden(); }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$ok = \Moko\Component\MokoSuite\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'));
$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('#__mokosuite_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() public function saveAutomation()
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
@@ -410,12 +654,13 @@ class DisplayController extends BaseController
'trigger_event' => $input->getString('trigger_event', 'ticket_created'), 'trigger_event' => $input->getString('trigger_event', 'ticket_created'),
'conditions' => $input->getRaw('conditions', '[]'), 'conditions' => $input->getRaw('conditions', '[]'),
'actions' => $input->getRaw('actions', '[]'), 'actions' => $input->getRaw('actions', '[]'),
'behavior' => $input->getString('behavior', 'append'),
'enabled' => 1, 'enabled' => 1,
'ordering' => 0, 'ordering' => 0,
]; ];
$id = $input->getInt('id', 0); $id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_automation', $data, 'id'); } if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_automation', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_automation', $data, 'id'); } else { $db->insertObject('#__mokosuite_ticket_automation', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]); $this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
} }
@@ -424,7 +669,7 @@ class DisplayController extends BaseController
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$db = Factory::getDbo(); $db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']); $this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
} }
@@ -434,12 +679,25 @@ class DisplayController extends BaseController
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$db = Factory::getDbo(); $db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->update('#__mokowaas_ticket_automation') $db->setQuery($db->getQuery(true)->update('#__mokosuite_ticket_automation')
->set('enabled = ' . $input->getInt('enabled', 0)) ->set('enabled = ' . $input->getInt('enabled', 0))
->where('id = ' . $input->getInt('id', 0)))->execute(); ->where('id = ' . $input->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']); $this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
} }
public function reorderAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$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('#__mokosuite_ticket_automation') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
// ================================================================== // ==================================================================
// Settings Import/Export (#132) // Settings Import/Export (#132)
// ================================================================== // ==================================================================
@@ -457,8 +715,8 @@ class DisplayController extends BaseController
$db = Factory::getDbo(); $db = Factory::getDbo();
$settings = []; $settings = [];
// Export all MokoWaaS plugin params // Export all MokoSuite plugin params
$plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_offline']; $plugins = ['mokosuite', 'mokosuite_firewall', 'mokosuite_tenant', 'mokosuite_devtools', 'mokosuite_offline'];
foreach ($plugins as $element) foreach ($plugins as $element)
{ {
@@ -478,7 +736,7 @@ class DisplayController extends BaseController
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
); );
$settings['component'] = json_decode($db->loadResult() ?? '{}', true); $settings['component'] = json_decode($db->loadResult() ?? '{}', true);
@@ -534,7 +792,7 @@ class DisplayController extends BaseController
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component']))) ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute(); )->execute();
$count++; $count++;
@@ -558,7 +816,7 @@ class DisplayController extends BaseController
} }
$days = Factory::getApplication()->getInput()->getInt('days', 30); $days = Factory::getApplication()->getInput()->getInt('days', 30);
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
$this->jsonResponse($model->purgeLogs($days)); $this->jsonResponse($model->purgeLogs($days));
} }
@@ -574,7 +832,7 @@ class DisplayController extends BaseController
} }
$ip = Factory::getApplication()->getInput()->getString('ip', ''); $ip = Factory::getApplication()->getInput()->getString('ip', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
$this->jsonResponse($model->banIp($ip)); $this->jsonResponse($model->banIp($ip));
} }
@@ -594,7 +852,7 @@ class DisplayController extends BaseController
} }
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
$action = $input->getString('action', 'deny'); $action = $input->getString('action', 'deny');
if ($action === 'create') if ($action === 'create')
@@ -640,7 +898,7 @@ class DisplayController extends BaseController
return; return;
} }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
$this->jsonResponse($model->exportUserData( $this->jsonResponse($model->exportUserData(
Factory::getApplication()->getInput()->getInt('user_id', 0) Factory::getApplication()->getInput()->getInt('user_id', 0)
@@ -655,7 +913,7 @@ class DisplayController extends BaseController
{ {
Session::checkToken() or die(Text::_('JINVALID_TOKEN')); Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) if (!$this->checkAcl('mokosuite.tickets'))
{ {
$this->jsonForbidden(); $this->jsonForbidden();
return; return;
@@ -682,19 +940,19 @@ class DisplayController extends BaseController
// ================================================================== // ==================================================================
/** /**
* Check a MokoWaaS ACL permission for the current user. * Check a MokoSuite ACL permission for the current user.
*/ */
private function checkAcl(string $action): bool private function checkAcl(string $action): bool
{ {
$user = Factory::getApplication()->getIdentity(); $user = Factory::getApplication()->getIdentity();
// Super admins always pass // Super admins always pass
if ($user->authorise('core.admin', 'com_mokowaas')) if ($user->authorise('core.admin', 'com_mokosuite'))
{ {
return true; return true;
} }
return $user->authorise($action, 'com_mokowaas'); return $user->authorise($action, 'com_mokosuite');
} }
/** /**
@@ -714,6 +972,7 @@ class DisplayController extends BaseController
private function jsonForbidden(): void private function jsonForbidden(): void
{ {
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); $this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
return; return;
} }
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -21,7 +21,7 @@ class DashboardModel extends BaseDatabaseModel
* Provides icon, category, and description for dashboard display. * Provides icon, category, and description for dashboard display.
*/ */
private const PLUGIN_META = [ private const PLUGIN_META = [
'mokowaas' => [ 'mokosuite' => [
'icon' => 'icon-shield-alt', 'icon' => 'icon-shield-alt',
'category' => 'core', 'category' => 'core',
'label' => 'Core', 'label' => 'Core',
@@ -29,7 +29,7 @@ class DashboardModel extends BaseDatabaseModel
'protected' => true, 'protected' => true,
'configure_only' => false, 'configure_only' => false,
], ],
'mokowaas_firewall' => [ 'mokosuite_firewall' => [
'icon' => 'icon-lock', 'icon' => 'icon-lock',
'category' => 'security', 'category' => 'security',
'label' => 'Firewall', 'label' => 'Firewall',
@@ -37,7 +37,7 @@ class DashboardModel extends BaseDatabaseModel
'protected' => false, 'protected' => false,
'configure_only' => false, 'configure_only' => false,
], ],
'mokowaas_tenant' => [ 'mokosuite_tenant' => [
'icon' => 'icon-users', 'icon' => 'icon-users',
'category' => 'security', 'category' => 'security',
'label' => 'Tenant Restrictions', 'label' => 'Tenant Restrictions',
@@ -45,7 +45,7 @@ class DashboardModel extends BaseDatabaseModel
'protected' => false, 'protected' => false,
'configure_only' => false, 'configure_only' => false,
], ],
'mokowaas_offline' => [ 'mokosuite_offline' => [
'icon' => 'icon-globe', 'icon' => 'icon-globe',
'category' => 'security', 'category' => 'security',
'label' => 'Offline Bypass', 'label' => 'Offline Bypass',
@@ -53,7 +53,7 @@ class DashboardModel extends BaseDatabaseModel
'protected' => false, 'protected' => false,
'configure_only' => true, 'configure_only' => true,
], ],
'mokowaas_devtools' => [ 'mokosuite_devtools' => [
'icon' => 'icon-wrench', 'icon' => 'icon-wrench',
'category' => 'tools', 'category' => 'tools',
'label' => 'Developer Tools', 'label' => 'Developer Tools',
@@ -61,7 +61,7 @@ class DashboardModel extends BaseDatabaseModel
'protected' => false, 'protected' => false,
'configure_only' => true, 'configure_only' => true,
], ],
'mokowaasdemo' => [ 'mokosuitedemo' => [
'icon' => 'icon-undo', 'icon' => 'icon-undo',
'category' => 'content', 'category' => 'content',
'label' => 'Demo Reset Task', 'label' => 'Demo Reset Task',
@@ -69,11 +69,11 @@ class DashboardModel extends BaseDatabaseModel
'protected' => false, 'protected' => false,
'configure_only' => true, 'configure_only' => true,
], ],
'mokowaassync' => [ 'mokosuitesync' => [
'icon' => 'icon-sync', 'icon' => 'icon-sync',
'category' => 'content', 'category' => 'content',
'label' => 'Content Sync Task', 'label' => 'Content Sync Task',
'description' => 'Scheduled content synchronisation to remote MokoWaaS sites.', 'description' => 'Scheduled content synchronisation to remote MokoSuite sites.',
'protected' => false, 'protected' => false,
'configure_only' => true, 'configure_only' => true,
], ],
@@ -92,7 +92,7 @@ class DashboardModel extends BaseDatabaseModel
]; ];
/** /**
* Discover all installed MokoWaaS plugins. * Discover all installed MokoSuite plugins.
* *
* @return array Plugin rows enriched with dashboard metadata. * @return array Plugin rows enriched with dashboard metadata.
*/ */
@@ -114,20 +114,20 @@ class DashboardModel extends BaseDatabaseModel
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where([ ->where([
'(' . '(' .
// System plugins: mokowaas, mokowaas_* // System plugins: mokosuite, mokosuite_*
'(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system')
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite')
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\_%') . ')' . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite\_%') . ')'
. ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokowaas_monitor') . ')' . ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokosuite_monitor') . ')'
// Webservices plugins // Webservices plugins
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices')
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ')' . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite') . ')'
// Task plugins // Task plugins
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas%') . ')' . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite%') . ')'
. ')', . ')',
]) ])
->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC'); ->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC');
@@ -190,11 +190,11 @@ class DashboardModel extends BaseDatabaseModel
$config = $app->getConfig(); $config = $app->getConfig();
$db = $this->getDatabase(); $db = $this->getDatabase();
// Get MokoWaaS package version // Get MokoSuite package version
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('manifest_cache')) ->select($db->quoteName('manifest_cache'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('package')); ->where($db->quoteName('type') . ' = ' . $db->quote('package'));
$db->setQuery($query); $db->setQuery($query);
$pkgCache = json_decode($db->loadResult() ?? '{}'); $pkgCache = json_decode($db->loadResult() ?? '{}');
@@ -204,7 +204,7 @@ class DashboardModel extends BaseDatabaseModel
'joomla_version' => (new Version())->getShortVersion(), 'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'db_type' => $db->getServerType(), 'db_type' => $db->getServerType(),
'mokowaas_version' => $pkgCache->version ?? '—', 'mokosuite_version' => $pkgCache->version ?? '—',
'debug' => (bool) $config->get('debug'), 'debug' => (bool) $config->get('debug'),
'offline' => (bool) $config->get('offline'), 'offline' => (bool) $config->get('offline'),
'sef' => (bool) $config->get('sef'), 'sef' => (bool) $config->get('sef'),
@@ -213,7 +213,7 @@ class DashboardModel extends BaseDatabaseModel
} }
/** /**
* Get installed MokoWaaS component and modules with versions. * Get installed MokoSuite component and modules with versions.
* *
* @return array Array of extension objects with name, element, type, version. * @return array Array of extension objects with name, element, type, version.
*/ */
@@ -232,10 +232,10 @@ class DashboardModel extends BaseDatabaseModel
->where('(' ->where('('
// The component // The component
. '(' . $db->quoteName('type') . ' = ' . $db->quote('component') . '(' . $db->quoteName('type') . ' = ' . $db->quote('component')
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokowaas') . ')' . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuite') . ')'
// Admin modules // Admin modules
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokowaas%') . ')' . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuite%') . ')'
. ')') . ')')
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC'); ->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
@@ -272,7 +272,7 @@ class DashboardModel extends BaseDatabaseModel
{ {
$db = $this->getDatabase(); $db = $this->getDatabase();
// Verify the extension exists and is a MokoWaaS plugin // Verify the extension exists and is a MokoSuite plugin
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select([$db->quoteName('element'), $db->quoteName('protected')]) ->select([$db->quoteName('element'), $db->quoteName('protected')])
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
@@ -287,7 +287,7 @@ class DashboardModel extends BaseDatabaseModel
} }
// Don't allow disabling protected/core plugins // Don't allow disabling protected/core plugins
if (!$enabled && ((int) $ext->protected || $ext->element === 'mokowaas')) if (!$enabled && ((int) $ext->protected || $ext->element === 'mokosuite'))
{ {
return ['success' => false, 'message' => 'This plugin is protected and cannot be disabled.']; return ['success' => false, 'message' => 'This plugin is protected and cannot be disabled.'];
} }
@@ -425,7 +425,7 @@ class DashboardModel extends BaseDatabaseModel
if (str_contains($row->element, 'sync')) if (str_contains($row->element, 'sync'))
{ {
$meta['label'] = 'Content Sync Task'; $meta['label'] = 'Content Sync Task';
$meta['description'] = 'Scheduled content synchronisation to remote MokoWaaS sites.'; $meta['description'] = 'Scheduled content synchronisation to remote MokoSuite sites.';
} }
elseif (str_contains($row->element, 'demo')) elseif (str_contains($row->element, 'demo'))
{ {
@@ -544,7 +544,7 @@ class DashboardModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_waf_log')) ->from($db->quoteName('#__mokosuite_waf_log'))
->order($db->quoteName('created') . ' DESC') ->order($db->quoteName('created') . ' DESC')
->setLimit($limit); ->setLimit($limit);
$db->setQuery($query); $db->setQuery($query);
@@ -567,7 +567,7 @@ class DashboardModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$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('#__mokowaas_waf_log') . " FROM " . $db->quoteName('#__mokosuite_waf_log')
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)" . " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
. " GROUP BY day ORDER BY day" . " GROUP BY day ORDER BY day"
); );
@@ -0,0 +1,80 @@
<?php
namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class ErpReportsModel extends BaseDatabaseModel
{
public function getSalesReport(string $from, string $to, string $groupBy = 'month'): array
{
$db = $this->getDatabase();
$fmt = match ($groupBy) { 'day' => '%Y-%m-%d', 'week' => '%Y-W%v', 'year' => '%Y', default => '%Y-%m' };
$db->setQuery($db->getQuery(true)
->select('DATE_FORMAT(inv.created, ' . $db->quote($fmt) . ') AS period')
->select('COUNT(*) AS invoice_count, COALESCE(SUM(inv.total), 0) AS revenue, COALESCE(SUM(inv.amount_paid), 0) AS collected')
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
->where($db->quoteName('inv.type') . ' = ' . $db->quote('standard'))
->group('period')->order('period ASC'));
return $db->loadObjectList() ?: [];
}
public function getTopCustomers(string $from, string $to, int $limit = 10): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)
->select('cd.name AS contact_name, COALESCE(SUM(inv.total), 0) AS total_revenue, COUNT(*) AS invoice_count')
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id')
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
->group('inv.contact_id')->order('total_revenue DESC'), 0, $limit);
return $db->loadObjectList() ?: [];
}
public function getTopProducts(string $from, string $to, int $limit = 10): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)
->select('p.sku, c.title AS product_name, COALESCE(SUM(ii.quantity), 0) AS qty_sold, COALESCE(SUM(ii.line_total), 0) AS revenue')
->from($db->quoteName('#__mokosuite_erp_invoice_items', 'ii'))
->join('INNER', $db->quoteName('#__mokosuite_erp_invoices', 'inv') . ' ON inv.id = ii.invoice_id')
->join('LEFT', $db->quoteName('#__mokosuite_erp_products', 'p') . ' ON p.id = ii.product_id')
->join('LEFT', $db->quoteName('#__content', 'c') . ' ON c.id = p.article_id')
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
->where($db->quoteName('ii.product_id') . ' IS NOT NULL')
->group('ii.product_id')->order('revenue DESC'), 0, $limit);
return $db->loadObjectList() ?: [];
}
public function getPipelineReport(string $from, string $to): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)
->select('status, COUNT(*) AS cnt, COALESCE(SUM(value), 0) AS total_value')
->from($db->quoteName('#__mokosuite_erp_deals'))
->where($db->quoteName('created') . ' >= ' . $db->quote($from))
->where($db->quoteName('created') . ' <= ' . $db->quote($to . ' 23:59:59'))
->group('status'));
return $db->loadObjectList('status') ?: [];
}
public function getAgingReceivables(): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)
->select('inv.id, inv.ref, inv.total, inv.amount_paid, inv.due_date, (inv.total - inv.amount_paid) AS balance, DATEDIFF(CURDATE(), inv.due_date) AS days_overdue')
->select('cd.name AS contact_name')
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id')
->where($db->quoteName('inv.status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('partial') . ',' . $db->quote('overdue') . ')')
->where('(inv.total - inv.amount_paid) > 0')
->order('days_overdue DESC'));
return $db->loadObjectList() ?: [];
}
}
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -48,6 +48,12 @@ class ExtensionsModel extends BaseDatabaseModel
$remoteVersion = $release['version'] ?? ''; $remoteVersion = $release['version'] ?? '';
$downloadUrl = $release['download_url'] ?? ''; $downloadUrl = $release['download_url'] ?? '';
// Skip extensions with no release available and not installed
if (empty($remoteVersion) && $localVersion === null)
{
continue;
}
$status = 'not_installed'; $status = 'not_installed';
if ($localVersion !== null) if ($localVersion !== null)
@@ -62,6 +68,9 @@ class ExtensionsModel extends BaseDatabaseModel
$extensionId = $this->getExtensionId($entry['element']); $extensionId = $this->getExtensionId($entry['element']);
$needsDlid = $release['needs_dlid'] ?? false;
$hasDlid = $needsDlid && $extensionId ? $this->hasDownloadKey($entry['element']) : false;
$packages[] = (object) [ $packages[] = (object) [
'label' => $entry['name'], 'label' => $entry['name'],
'description' => $entry['description'], 'description' => $entry['description'],
@@ -76,6 +85,9 @@ class ExtensionsModel extends BaseDatabaseModel
'article_url' => $entry['article'] ?? '', 'article_url' => $entry['article'] ?? '',
'protected' => ($entry['protected'] ?? 'false') === 'true', 'protected' => ($entry['protected'] ?? 'false') === 'true',
'extension_id' => $extensionId, 'extension_id' => $extensionId,
'needs_dlid' => $needsDlid,
'has_dlid' => $hasDlid,
'has_stable' => $release['has_stable'] ?? false,
]; ];
} }
@@ -92,7 +104,7 @@ class ExtensionsModel extends BaseDatabaseModel
public function installFromUrl(string $url): array public function installFromUrl(string $url): array
{ {
$tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp'); $tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
$tmpFile = $tmpPath . '/mokowaas_install_' . md5($url) . '.zip'; $tmpFile = $tmpPath . '/mokosuite_install_' . md5($url) . '.zip';
try try
{ {
@@ -148,7 +160,7 @@ class ExtensionsModel extends BaseDatabaseModel
return $this->catalogCache; return $this->catalogCache;
} }
$catalogFile = JPATH_ADMINISTRATOR . '/components/com_mokowaas/catalog.xml'; $catalogFile = JPATH_ADMINISTRATOR . '/components/com_mokosuite/catalog.xml';
if (!file_exists($catalogFile)) if (!file_exists($catalogFile))
{ {
@@ -226,13 +238,36 @@ class ExtensionsModel extends BaseDatabaseModel
return []; return [];
} }
// Find the highest version entry // Determine site's update channel preference
$channel = 'dev'; // default to dev — show everything
$hasStable = false;
$hasDev = false;
// Find the best version entry, preferring the site's channel
$bestVersion = '0.0.0'; $bestVersion = '0.0.0';
$downloadUrl = ''; $downloadUrl = '';
$needsDlid = false;
foreach ($xml->update as $update) foreach ($xml->update as $update)
{ {
$ver = (string) ($update->version ?? ''); $ver = (string) ($update->version ?? '');
$tag = '';
// Check for <tags><tag> element
if (isset($update->tags->tag))
{
$tag = (string) $update->tags->tag;
}
if ($tag === 'stable')
{
$hasStable = true;
}
if ($tag === 'dev')
{
$hasDev = true;
}
if ($ver === '' || version_compare($ver, $bestVersion, '<=')) if ($ver === '' || version_compare($ver, $bestVersion, '<='))
{ {
@@ -241,10 +276,15 @@ class ExtensionsModel extends BaseDatabaseModel
$bestVersion = $ver; $bestVersion = $ver;
// Get download URL from <downloads><downloadurl>
if (isset($update->downloads->downloadurl)) if (isset($update->downloads->downloadurl))
{ {
$downloadUrl = (string) $update->downloads->downloadurl; $downloadUrl = (string) $update->downloads->downloadurl;
// Check if download URL contains dlid placeholder
if (str_contains($downloadUrl, 'dlid='))
{
$needsDlid = true;
}
} }
} }
@@ -256,6 +296,9 @@ class ExtensionsModel extends BaseDatabaseModel
return [ return [
'version' => $bestVersion, 'version' => $bestVersion,
'download_url' => $downloadUrl, 'download_url' => $downloadUrl,
'has_stable' => $hasStable,
'has_dev' => $hasDev,
'needs_dlid' => $needsDlid,
]; ];
} }
@@ -299,6 +342,33 @@ class ExtensionsModel extends BaseDatabaseModel
return $versions; return $versions;
} }
/**
* Check if an extension has a download key configured.
*/
private function hasDownloadKey(string $element): bool
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('us.extra_query'))
->from($db->quoteName('#__update_sites', 'us'))
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id')
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('e.element') . ' = ' . $db->quote($element));
$db->setQuery($query, 0, 1);
$extraQuery = (string) $db->loadResult();
return !empty($extraQuery) && str_contains($extraQuery, 'dlid=');
}
catch (\Throwable $e)
{
return false;
}
}
/** /**
* Get the extension_id for an element (for uninstall links). * Get the extension_id for an element (for uninstall links).
* *
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -63,7 +63,7 @@ class HtaccessModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')); ->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query); $db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}'); $params = new Registry($db->loadResult() ?? '{}');
@@ -89,7 +89,7 @@ class HtaccessModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')); ->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query); $db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}'); $params = new Registry($db->loadResult() ?? '{}');
@@ -107,7 +107,7 @@ class HtaccessModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute(); )->execute();
@@ -135,7 +135,7 @@ class HtaccessModel extends BaseDatabaseModel
public function saveHtaccess(string $content): array public function saveHtaccess(string $content): array
{ {
$path = JPATH_ROOT . '/.htaccess'; $path = JPATH_ROOT . '/.htaccess';
$backup = JPATH_ROOT . '/.htaccess.mokowaas.bak'; $backup = JPATH_ROOT . '/.htaccess.mokosuite.bak';
try try
{ {
@@ -158,7 +158,7 @@ class HtaccessModel extends BaseDatabaseModel
return ['success' => false, 'message' => '.htaccess is not writable.']; return ['success' => false, 'message' => '.htaccess is not writable.'];
} }
return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokowaas.bak']; return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokosuite.bak'];
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
@@ -178,9 +178,9 @@ class HtaccessModel extends BaseDatabaseModel
{ {
$lines = []; $lines = [];
$lines[] = '##'; $lines[] = '##';
$lines[] = '## MokoWaaS Generated .htaccess'; $lines[] = '## MokoSuite Generated .htaccess';
$lines[] = '## Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC'; $lines[] = '## Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC';
$lines[] = '## DO NOT EDIT — regenerate from MokoWaaS > .htaccess Maker'; $lines[] = '## DO NOT EDIT — regenerate from MokoSuite > .htaccess Maker';
$lines[] = '##'; $lines[] = '##';
$lines[] = ''; $lines[] = '';
@@ -412,7 +412,7 @@ class HtaccessModel extends BaseDatabaseModel
public function generateNginx(array $opts): string public function generateNginx(array $opts): string
{ {
$lines = []; $lines = [];
$lines[] = '## MokoWaaS Generated NginX Configuration'; $lines[] = '## MokoSuite Generated NginX Configuration';
$lines[] = '## Add these directives inside your server { } block'; $lines[] = '## Add these directives inside your server { } block';
$lines[] = ''; $lines[] = '';
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,10 +16,10 @@ use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
/** /**
* Importer for migrating from Akeeba Admin Tools to MokoWaaS. * Importer for migrating from Akeeba Admin Tools to MokoSuite.
* *
* Reads Admin Tools WAF config, htaccess settings, IP blocklists, * Reads Admin Tools WAF config, htaccess settings, IP blocklists,
* and security headers maps them to MokoWaaS firewall plugin params * and security headers maps them to MokoSuite firewall plugin params
* and htaccess maker options. * and htaccess maker options.
* *
* @since 02.32.00 * @since 02.32.00
@@ -94,7 +94,7 @@ class ImportModel extends BaseDatabaseModel
} }
/** /**
* Import Admin Tools settings into MokoWaaS. * Import Admin Tools settings into MokoSuite.
*/ */
public function importAdminTools(): array public function importAdminTools(): array
{ {
@@ -111,7 +111,7 @@ class ImportModel extends BaseDatabaseModel
if (!empty($firewallParams)) if (!empty($firewallParams))
{ {
$this->mergePluginParams('mokowaas_firewall', 'system', $firewallParams); $this->mergePluginParams('mokosuite_firewall', 'system', $firewallParams);
$results['firewall'] = \count($firewallParams); $results['firewall'] = \count($firewallParams);
} }
@@ -260,7 +260,7 @@ class ImportModel extends BaseDatabaseModel
} }
/** /**
* Map Admin Tools WAF config to MokoWaaS firewall plugin params. * Map Admin Tools WAF config to MokoSuite firewall plugin params.
*/ */
private function mapWafToFirewall(array $waf): array private function mapWafToFirewall(array $waf): array
{ {
@@ -332,7 +332,7 @@ class ImportModel extends BaseDatabaseModel
} }
/** /**
* Map Admin Tools config to MokoWaaS htaccess maker options. * Map Admin Tools config to MokoSuite htaccess maker options.
*/ */
private function mapToHtaccess(array $storage, array $waf): array private function mapToHtaccess(array $storage, array $waf): array
{ {
@@ -448,7 +448,7 @@ class ImportModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')); ->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query); $db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}'); $params = new Registry($db->loadResult() ?? '{}');
@@ -466,7 +466,7 @@ class ImportModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute(); )->execute();
} }
@@ -481,7 +481,7 @@ class ImportModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')); ->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query); $db->setQuery($query);
@@ -513,7 +513,7 @@ class ImportModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute(); )->execute();
@@ -541,7 +541,7 @@ class ImportModel extends BaseDatabaseModel
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
)->execute(); )->execute();
Log::add('Admin Tools component and plugins disabled after MokoWaaS import', Log::INFO, 'mokowaas'); Log::add('Admin Tools component and plugins disabled after MokoSuite import', Log::INFO, 'mokosuite');
} }
// ================================================================== // ==================================================================
@@ -619,7 +619,7 @@ class ImportModel extends BaseDatabaseModel
)->execute(); )->execute();
$result['message'] .= ' Akeeba Ticket System has been disabled.'; $result['message'] .= ' Akeeba Ticket System has been disabled.';
Log::add('Akeeba Ticket System disabled after MokoWaaS import', Log::INFO, 'mokowaas'); Log::add('Akeeba Ticket System disabled after MokoSuite import', Log::INFO, 'mokosuite');
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
@@ -644,7 +644,7 @@ class ImportModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
); );
$params = new Registry($db->loadResult() ?? '{}'); $params = new Registry($db->loadResult() ?? '{}');
@@ -666,7 +666,7 @@ class ImportModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
); );
$params = new Registry($db->loadResult() ?? '{}'); $params = new Registry($db->loadResult() ?? '{}');
@@ -676,13 +676,13 @@ class ImportModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute(); )->execute();
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -36,7 +36,7 @@ class MaintenanceModel extends BaseDatabaseModel
'engine' => $t->Engine, 'engine' => $t->Engine,
'size_mb' => $sizeMb, 'size_mb' => $sizeMb,
'overhead_kb' => $overheadKb, 'overhead_kb' => $overheadKb,
'is_moko' => str_contains($t->Name, 'mokowaas'), 'is_moko' => str_contains($t->Name, 'mokosuite'),
]; ];
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -30,7 +30,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->quoteName('u.username'), $db->quoteName('u.username'),
$db->quoteName('p.name', 'processed_by_name'), $db->quoteName('p.name', 'processed_by_name'),
]) ])
->from($db->quoteName('#__mokowaas_data_requests', 'r')) ->from($db->quoteName('#__mokosuite_data_requests', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id') ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by'); ->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by');
@@ -68,7 +68,7 @@ class PrivacyModel extends BaseDatabaseModel
'created' => Factory::getDate()->toSql(), 'created' => Factory::getDate()->toSql(),
]; ];
$db->insertObject('#__mokowaas_data_requests', $row, 'id'); $db->insertObject('#__mokosuite_data_requests', $row, 'id');
return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id]; return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id];
} }
@@ -90,7 +90,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_data_requests')) ->from($db->quoteName('#__mokosuite_data_requests'))
->where($db->quoteName('id') . ' = ' . $requestId) ->where($db->quoteName('id') . ' = ' . $requestId)
); );
$request = $db->loadObject(); $request = $db->loadObject();
@@ -104,7 +104,7 @@ class PrivacyModel extends BaseDatabaseModel
{ {
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests')) ->update($db->quoteName('#__mokosuite_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('denied')) ->set($db->quoteName('status') . ' = ' . $db->quote('denied'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id) ->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql())) ->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
@@ -117,7 +117,7 @@ class PrivacyModel extends BaseDatabaseModel
// Mark as processing // Mark as processing
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests')) ->update($db->quoteName('#__mokosuite_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('processing')) ->set($db->quoteName('status') . ' = ' . $db->quote('processing'))
->where($db->quoteName('id') . ' = ' . $requestId) ->where($db->quoteName('id') . ' = ' . $requestId)
)->execute(); )->execute();
@@ -143,7 +143,7 @@ class PrivacyModel extends BaseDatabaseModel
// Mark completed // Mark completed
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests')) ->update($db->quoteName('#__mokosuite_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('completed')) ->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id) ->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql())) ->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
@@ -201,7 +201,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select(['id', 'subject', 'body', 'status', 'priority', 'created']) ->select(['id', 'subject', 'body', 'status', 'priority', 'created'])
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId) ->where($db->quoteName('created_by') . ' = ' . $userId)
); );
$data['tickets'] = $db->loadObjectList() ?: []; $data['tickets'] = $db->loadObjectList() ?: [];
@@ -210,7 +210,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select(['r.id', 'r.ticket_id', 'r.body', 'r.created']) ->select(['r.id', 'r.ticket_id', 'r.body', 'r.created'])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r')) ->from($db->quoteName('#__mokosuite_ticket_replies', 'r'))
->where($db->quoteName('r.user_id') . ' = ' . $userId) ->where($db->quoteName('r.user_id') . ' = ' . $userId)
); );
$data['ticket_replies'] = $db->loadObjectList() ?: []; $data['ticket_replies'] = $db->loadObjectList() ?: [];
@@ -219,7 +219,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_consent_log')) ->from($db->quoteName('#__mokosuite_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId) ->where($db->quoteName('user_id') . ' = ' . $userId)
->order('created ASC') ->order('created ASC')
); );
@@ -295,7 +295,7 @@ class PrivacyModel extends BaseDatabaseModel
// Anonymize ticket replies // Anonymize ticket replies
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_ticket_replies')) ->update($db->quoteName('#__mokosuite_ticket_replies'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]')) ->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]'))
->where($db->quoteName('user_id') . ' = ' . $userId) ->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute(); )->execute();
@@ -374,7 +374,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('id')) ->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId) ->where($db->quoteName('created_by') . ' = ' . $userId)
); );
$ticketIds = $db->loadColumn() ?: []; $ticketIds = $db->loadColumn() ?: [];
@@ -383,13 +383,13 @@ class PrivacyModel extends BaseDatabaseModel
{ {
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->delete($db->quoteName('#__mokowaas_ticket_replies')) ->delete($db->quoteName('#__mokosuite_ticket_replies'))
->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')') ->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')')
)->execute(); )->execute();
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->delete($db->quoteName('#__mokowaas_tickets')) ->delete($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId) ->where($db->quoteName('created_by') . ' = ' . $userId)
)->execute(); )->execute();
} }
@@ -397,7 +397,7 @@ class PrivacyModel extends BaseDatabaseModel
// Delete consent log // Delete consent log
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->delete($db->quoteName('#__mokowaas_consent_log')) ->delete($db->quoteName('#__mokosuite_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId) ->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute(); )->execute();
@@ -429,7 +429,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_consent_log')) ->from($db->quoteName('#__mokosuite_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId) ->where($db->quoteName('user_id') . ' = ' . $userId)
->order($db->quoteName('created') . ' DESC') ->order($db->quoteName('created') . ' DESC')
); );
@@ -450,7 +450,7 @@ class PrivacyModel extends BaseDatabaseModel
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
'created' => Factory::getDate()->toSql(), 'created' => Factory::getDate()->toSql(),
]; ];
$db->insertObject('#__mokowaas_consent_log', $row, 'id'); $db->insertObject('#__mokosuite_consent_log', $row, 'id');
} }
// ================================================================== // ==================================================================
@@ -466,7 +466,7 @@ class PrivacyModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_retention_policies')) ->from($db->quoteName('#__mokosuite_retention_policies'))
->order($db->quoteName('id') . ' ASC') ->order($db->quoteName('id') . ' ASC')
); );
@@ -508,7 +508,7 @@ class PrivacyModel extends BaseDatabaseModel
case 'waf_logs': case 'waf_logs':
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log')) ->delete($db->quoteName('#__mokosuite_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff)) ->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute(); )->execute();
$count = $db->getAffectedRows(); $count = $db->getAffectedRows();
@@ -528,7 +528,7 @@ class PrivacyModel extends BaseDatabaseModel
{ {
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets')) ->update($db->quoteName('#__mokosuite_tickets'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]')) ->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]'))
->where($db->quoteName('status') . ' = ' . $db->quote('closed')) ->where($db->quoteName('status') . ' = ' . $db->quote('closed'))
->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff)) ->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff))
@@ -565,12 +565,12 @@ class PrivacyModel extends BaseDatabaseModel
{ {
$results['policies_run']++; $results['policies_run']++;
$results['items_affected'] += $count; $results['items_affected'] += $count;
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokowaas'); Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokosuite');
} }
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
@@ -593,16 +593,16 @@ class PrivacyModel extends BaseDatabaseModel
try try
{ {
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests WHERE status = ' . $db->quote('pending')); $db->setQuery('SELECT COUNT(*) FROM #__mokosuite_data_requests WHERE status = ' . $db->quote('pending'));
$summary->pending_requests = (int) $db->loadResult(); $summary->pending_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests'); $db->setQuery('SELECT COUNT(*) FROM #__mokosuite_data_requests');
$summary->total_requests = (int) $db->loadResult(); $summary->total_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_consent_log'); $db->setQuery('SELECT COUNT(*) FROM #__mokosuite_consent_log');
$summary->consent_entries = (int) $db->loadResult(); $summary->consent_entries = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_retention_policies WHERE enabled = 1'); $db->setQuery('SELECT COUNT(*) FROM #__mokosuite_retention_policies WHERE enabled = 1');
$summary->policies_active = (int) $db->loadResult(); $summary->policies_active = (int) $db->loadResult();
} }
catch (\Throwable $e) {} catch (\Throwable $e) {}
@@ -1,18 +1,18 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Moko\Component\MokoWaaS\Administrator\Service\NotificationService; use Moko\Component\MokoSuite\Administrator\Service\NotificationService;
class TicketsModel extends BaseDatabaseModel class TicketsModel extends BaseDatabaseModel
{ {
@@ -26,30 +26,40 @@ class TicketsModel extends BaseDatabaseModel
->select([ ->select([
$db->quoteName('t.id'), $db->quoteName('t.id'),
$db->quoteName('t.subject'), $db->quoteName('t.subject'),
$db->quoteName('t.status'), $db->quoteName('t.status_id'),
$db->quoteName('t.priority'), $db->quoteName('t.priority_id'),
$db->quoteName('t.created'), $db->quoteName('t.created'),
$db->quoteName('t.modified'), $db->quoteName('t.modified'),
$db->quoteName('t.contact_id'),
$db->quoteName('t.sla_response_due'), $db->quoteName('t.sla_response_due'),
$db->quoteName('t.sla_resolution_due'), $db->quoteName('t.sla_resolution_due'),
$db->quoteName('t.sla_responded'), $db->quoteName('t.sla_responded'),
$db->quoteName('c.title', 'category_title'), $db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'), $db->quoteName('u.name', 'created_by_name'),
$db->quoteName('a.name', 'assigned_to_name'), $db->quoteName('ct.name', 'contact_name'),
$db->quoteName('st.title', 'status_title'),
$db->quoteName('st.alias', 'status_alias'),
$db->quoteName('st.color', 'status_color'),
$db->quoteName('pr.title', 'priority_title'),
$db->quoteName('pr.alias', 'priority_alias'),
$db->quoteName('pr.color', 'priority_color'),
$db->quoteName('st.is_closed', 'status_is_closed'),
]) ])
->from($db->quoteName('#__mokowaas_tickets', 't')) ->from($db->quoteName('#__mokosuite_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id') ->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to'); ->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 'st') . ' ON st.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id');
if (!empty($filters['status'])) if (!empty($filters['status_id']))
{ {
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($filters['status'])); $query->where($db->quoteName('t.status_id') . ' = ' . (int) $filters['status_id']);
} }
if (!empty($filters['priority'])) if (!empty($filters['priority_id']))
{ {
$query->where($db->quoteName('t.priority') . ' = ' . $db->quote($filters['priority'])); $query->where($db->quoteName('t.priority_id') . ' = ' . (int) $filters['priority_id']);
} }
if (!empty($filters['assigned_to'])) if (!empty($filters['assigned_to']))
@@ -62,12 +72,24 @@ class TicketsModel extends BaseDatabaseModel
$query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']); $query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']);
} }
if (!empty($filters['contact_id']))
{
$query->where($db->quoteName('t.contact_id') . ' = ' . (int) $filters['contact_id']);
}
$query->order($db->quoteName('t.created') . ' DESC'); $query->order($db->quoteName('t.created') . ' DESC');
$query->setLimit(50); $query->setLimit(50);
$db->setQuery($query); $db->setQuery($query);
$tickets = $db->loadObjectList() ?: [];
return $db->loadObjectList() ?: []; // Load assignees for each ticket
foreach ($tickets as $ticket)
{
$ticket->assignees = $this->getTicketAssignees((int) $ticket->id);
}
return $tickets;
} }
/** /**
@@ -82,12 +104,23 @@ class TicketsModel extends BaseDatabaseModel
$db->quoteName('c.title', 'category_title'), $db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'), $db->quoteName('u.name', 'created_by_name'),
$db->quoteName('u.email', 'created_by_email'), $db->quoteName('u.email', 'created_by_email'),
$db->quoteName('a.name', 'assigned_to_name'), $db->quoteName('ct.name', 'contact_name'),
$db->quoteName('ct.email_to', 'contact_email'),
$db->quoteName('ct.telephone', 'contact_phone'),
$db->quoteName('st.title', 'status_title'),
$db->quoteName('st.alias', 'status_alias'),
$db->quoteName('st.color', 'status_color'),
$db->quoteName('st.is_closed', 'status_is_closed'),
$db->quoteName('pr.title', 'priority_title'),
$db->quoteName('pr.alias', 'priority_alias'),
$db->quoteName('pr.color', 'priority_color'),
]) ])
->from($db->quoteName('#__mokowaas_tickets', 't')) ->from($db->quoteName('#__mokosuite_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id') ->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to') ->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 'st') . ' ON st.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id')
->where($db->quoteName('t.id') . ' = ' . $id); ->where($db->quoteName('t.id') . ' = ' . $id);
$db->setQuery($query); $db->setQuery($query);
$ticket = $db->loadObject(); $ticket = $db->loadObject();
@@ -103,7 +136,7 @@ class TicketsModel extends BaseDatabaseModel
$db->quoteName('r') . '.*', $db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'), $db->quoteName('u.name', 'user_name'),
]) ])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r')) ->from($db->quoteName('#__mokosuite_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id') ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where($db->quoteName('r.ticket_id') . ' = ' . $id) ->where($db->quoteName('r.ticket_id') . ' = ' . $id)
->order($db->quoteName('r.created') . ' ASC'); ->order($db->quoteName('r.created') . ' ASC');
@@ -113,6 +146,9 @@ class TicketsModel extends BaseDatabaseModel
// Reply count // Reply count
$ticket->reply_count = \count($ticket->replies); $ticket->reply_count = \count($ticket->replies);
// Load assignees (users + groups)
$ticket->assignees = $this->getTicketAssignees($id);
return $ticket; return $ticket;
} }
@@ -127,16 +163,23 @@ class TicketsModel extends BaseDatabaseModel
$user = Factory::getApplication()->getIdentity(); $user = Factory::getApplication()->getIdentity();
$now = Factory::getDate()->toSql(); $now = Factory::getDate()->toSql();
// Resolve default status/priority from lookup tables
$defaultStatus = $this->getDefaultStatus();
$defaultPriority = $this->getDefaultPriority();
$ticket = (object) [ $ticket = (object) [
'subject' => $data['subject'] ?? '', 'subject' => $data['subject'] ?? '',
'body' => $data['body'] ?? '', 'body' => $data['body'] ?? '',
'status' => 'open', 'status' => $defaultStatus->alias ?? 'open',
'priority' => $data['priority'] ?? 'normal', 'status_id' => (int) ($data['status_id'] ?? $defaultStatus->id ?? 1),
'category_id' => (int) ($data['category_id'] ?? 0) ?: null, 'priority' => $defaultPriority->alias ?? 'normal',
'created_by' => $user->id, 'priority_id' => (int) ($data['priority_id'] ?? $defaultPriority->id ?? 2),
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null, 'category_id' => (int) ($data['category_id'] ?? 0) ?: null,
'created' => $now, 'contact_id' => (int) ($data['contact_id'] ?? 0) ?: null,
'modified' => $now, 'created_by' => $user->id,
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null,
'created' => $now,
'modified' => $now,
]; ];
// Auto-assign from category // Auto-assign from category
@@ -144,7 +187,7 @@ class TicketsModel extends BaseDatabaseModel
{ {
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('auto_assign_user')) ->select($db->quoteName('auto_assign_user'))
->from($db->quoteName('#__mokowaas_ticket_categories')) ->from($db->quoteName('#__mokosuite_ticket_categories'))
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id); ->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
$db->setQuery($query); $db->setQuery($query);
$autoAssign = (int) $db->loadResult(); $autoAssign = (int) $db->loadResult();
@@ -160,7 +203,7 @@ class TicketsModel extends BaseDatabaseModel
{ {
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select([$db->quoteName('sla_response_minutes'), $db->quoteName('sla_resolution_minutes')]) ->select([$db->quoteName('sla_response_minutes'), $db->quoteName('sla_resolution_minutes')])
->from($db->quoteName('#__mokowaas_ticket_categories')) ->from($db->quoteName('#__mokosuite_ticket_categories'))
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id); ->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
$db->setQuery($query); $db->setQuery($query);
$sla = $db->loadObject(); $sla = $db->loadObject();
@@ -172,7 +215,30 @@ class TicketsModel extends BaseDatabaseModel
} }
} }
$db->insertObject('#__mokowaas_tickets', $ticket, 'id'); $db->insertObject('#__mokosuite_tickets', $ticket, 'id');
// Handle multi-assignee (users and groups)
$assignUsers = array_filter(array_map('intval', (array) ($data['assign_users'] ?? [])));
$assignGroups = array_filter(array_map('intval', (array) ($data['assign_groups'] ?? [])));
// Backward compat: single assigned_to becomes a user assignee
if (empty($assignUsers) && $ticket->assigned_to)
{
$assignUsers = [$ticket->assigned_to];
}
if (!empty($assignUsers) || !empty($assignGroups))
{
$this->setTicketAssignees((int) $ticket->id, $assignUsers, $assignGroups);
}
// Save custom field values
$fieldValues = (array) ($data['custom_fields'] ?? []);
if (!empty($fieldValues))
{
$this->saveFieldValues((int) $ticket->id, $fieldValues);
}
// Run automation + notifications // Run automation + notifications
$this->runAutomation('ticket_created', (int) $ticket->id); $this->runAutomation('ticket_created', (int) $ticket->id);
@@ -205,14 +271,14 @@ class TicketsModel extends BaseDatabaseModel
'created' => $now, 'created' => $now,
]; ];
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id'); $db->insertObject('#__mokosuite_ticket_replies', $reply, 'id');
// Mark SLA as responded only for staff replies (not customer self-replies) // Mark SLA as responded only for staff replies (not customer self-replies)
$ticket = $this->getTicket($ticketId); $ticket = $this->getTicket($ticketId);
$isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by; $isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by;
$updateQuery = $db->getQuery(true) $updateQuery = $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets')) ->update($db->quoteName('#__mokosuite_tickets'))
->set($db->quoteName('modified') . ' = ' . $db->quote($now)) ->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId); ->where($db->quoteName('id') . ' = ' . $ticketId);
@@ -240,62 +306,81 @@ class TicketsModel extends BaseDatabaseModel
} }
} }
/** @var bool Guard against automation recursion */
private bool $automationRunning = false;
/** /**
* Update ticket status. * Update ticket status by status ID (lookup table).
*/ */
public function updateStatus(int $ticketId, string $status): array public function updateStatus(int $ticketId, int $statusId): array
{ {
$valid = ['open', 'in_progress', 'waiting', 'resolved', 'closed'];
if (!\in_array($status, $valid, true))
{
return ['success' => false, 'message' => 'Invalid status.'];
}
try try
{ {
$db = $this->getDatabase(); $db = $this->getDatabase();
$now = Factory::getDate()->toSql(); $now = Factory::getDate()->toSql();
// Validate status ID against lookup table
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_statuses'))
->where($db->quoteName('id') . ' = ' . $statusId)
);
$status = $db->loadObject();
if (!$status)
{
return ['success' => false, 'message' => 'Invalid status.'];
}
// Capture old status for notification // Capture old status for notification
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('status')) ->select($db->quoteName('status_id'))
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('id') . ' = ' . $ticketId) ->where($db->quoteName('id') . ' = ' . $ticketId)
); );
$oldStatus = $db->loadResult() ?? ''; $oldStatusId = (int) $db->loadResult();
$sets = [ $sets = [
$db->quoteName('status') . ' = ' . $db->quote($status), $db->quoteName('status') . ' = ' . $db->quote($status->alias),
$db->quoteName('status_id') . ' = ' . $statusId,
$db->quoteName('modified') . ' = ' . $db->quote($now), $db->quoteName('modified') . ' = ' . $db->quote($now),
]; ];
if ($status === 'resolved') if ($status->is_closed)
{
$sets[] = $db->quoteName('resolved') . ' = ' . $db->quote($now);
}
if ($status === 'closed')
{ {
$sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now); $sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now);
} }
// Set resolved timestamp for "resolved" alias (backward compat)
if ($status->alias === 'resolved')
{
$sets[] = $db->quoteName('resolved') . ' = ' . $db->quote($now);
}
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets')) ->update($db->quoteName('#__mokosuite_tickets'))
->set($sets) ->set($sets)
->where($db->quoteName('id') . ' = ' . $ticketId) ->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute(); )->execute();
// Run automation + notifications // Run automation + notifications (with recursion guard)
$this->runAutomation('status_changed', $ticketId); if (!$this->automationRunning)
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status' => $oldStatus]); {
$this->automationRunning = true;
$this->runAutomation('status_changed', $ticketId);
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status_id' => $oldStatusId]);
$this->automationRunning = false;
}
return ['success' => true, 'message' => 'Status updated to ' . $status . '.']; return ['success' => true, 'message' => 'Status updated to ' . $status->title . '.'];
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
$this->automationRunning = false;
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()]; return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
} }
} }
@@ -309,7 +394,7 @@ class TicketsModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_categories')) ->from($db->quoteName('#__mokosuite_ticket_categories'))
->where($db->quoteName('published') . ' = 1') ->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC') ->order($db->quoteName('ordering') . ' ASC')
); );
@@ -317,6 +402,278 @@ class TicketsModel extends BaseDatabaseModel
return $db->loadObjectList() ?: []; return $db->loadObjectList() ?: [];
} }
/**
* Get assignees for a ticket (users and groups with resolved names).
*/
public function getTicketAssignees(int $ticketId): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_assignees'))
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
);
$rows = $db->loadObjectList() ?: [];
foreach ($rows as $row)
{
if ($row->assignee_type === 'user')
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('name'))
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
);
$row->name = (string) $db->loadResult() ?: 'User #' . $row->assignee_id;
}
else
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__usergroups'))
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
);
$row->name = (string) $db->loadResult() ?: 'Group #' . $row->assignee_id;
}
}
return $rows;
}
/**
* Set assignees for a ticket (replaces existing assignments).
*
* @param int $ticketId Ticket ID
* @param array $users Array of user IDs
* @param array $groups Array of user group IDs
*/
public function setTicketAssignees(int $ticketId, array $users = [], array $groups = []): void
{
$db = $this->getDatabase();
// Clear existing
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokosuite_ticket_assignees'))
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
)->execute();
// Insert users
foreach ($users as $uid)
{
$uid = (int) $uid;
if ($uid > 0)
{
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
'ticket_id' => $ticketId,
'assignee_type' => 'user',
'assignee_id' => $uid,
]);
}
}
// Insert groups
foreach ($groups as $gid)
{
$gid = (int) $gid;
if ($gid > 0)
{
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
'ticket_id' => $ticketId,
'assignee_type' => 'group',
'assignee_id' => $gid,
]);
}
}
}
/**
* Get all published Joomla contact records for ticket linking.
*/
public function getContacts(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('name')])
->from($db->quoteName('#__contact_details'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('name') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get the default ticket status.
*/
public function getDefaultStatus(): ?object
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_statuses'))
->where($db->quoteName('is_default') . ' = 1')
->setLimit(1)
);
return $db->loadObject() ?: null;
}
/**
* Get the default ticket priority.
*/
public function getDefaultPriority(): ?object
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_priorities'))
->where($db->quoteName('is_default') . ' = 1')
->setLimit(1)
);
return $db->loadObject() ?: null;
}
/**
* Get all ticket statuses.
*/
public function getStatuses(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_statuses'))
->order($db->quoteName('ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get all ticket priorities.
*/
public function getPriorities(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuite_ticket_priorities'))
->order($db->quoteName('ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get Joomla custom field groups assigned to a ticket category.
*/
public function getFieldGroupsForCategory(int $categoryId): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('fg.id'), $db->quoteName('fg.title')])
->from($db->quoteName('#__mokosuite_ticket_category_field_groups', 'cfg'))
->innerJoin($db->quoteName('#__fields_groups', 'fg') . ' ON fg.id = cfg.field_group_id')
->where($db->quoteName('cfg.category_id') . ' = ' . $categoryId)
->where($db->quoteName('fg.state') . ' = 1')
->order($db->quoteName('fg.ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get Joomla custom fields for given field group IDs (context: com_mokosuite.ticket).
*/
public function getFieldsForGroups(array $groupIds): array
{
if (empty($groupIds))
{
return [];
}
$db = $this->getDatabase();
$ids = implode(',', array_map('intval', $groupIds));
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__fields'))
->where($db->quoteName('context') . ' = ' . $db->quote('com_mokosuite.ticket'))
->where($db->quoteName('group_id') . ' IN (' . $ids . ')')
->where($db->quoteName('state') . ' = 1')
->order($db->quoteName('ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get custom field values for a ticket.
*/
public function getFieldValues(int $ticketId): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('field_id'), $db->quoteName('value')])
->from($db->quoteName('#__fields_values'))
->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId))
);
$rows = $db->loadObjectList() ?: [];
$values = [];
foreach ($rows as $row)
{
$values[(int) $row->field_id] = $row->value;
}
return $values;
}
/**
* Save custom field values for a ticket.
*/
public function saveFieldValues(int $ticketId, array $fieldValues): void
{
$db = $this->getDatabase();
foreach ($fieldValues as $fieldId => $value)
{
$fieldId = (int) $fieldId;
// Delete existing
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__fields_values'))
->where($db->quoteName('field_id') . ' = ' . $fieldId)
->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId))
)->execute();
// Insert new value (skip empty)
if ($value !== '' && $value !== null)
{
$db->insertObject('#__fields_values', (object) [
'field_id' => $fieldId,
'item_id' => (string) $ticketId,
'value' => $value,
]);
}
}
}
/** /**
* Get canned responses, optionally filtered by category. * Get canned responses, optionally filtered by category.
*/ */
@@ -325,7 +682,7 @@ class TicketsModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_canned')) ->from($db->quoteName('#__mokosuite_ticket_canned'))
->order($db->quoteName('ordering') . ' ASC'); ->order($db->quoteName('ordering') . ' ASC');
if ($categoryId) if ($categoryId)
@@ -342,25 +699,26 @@ class TicketsModel extends BaseDatabaseModel
/** /**
* Get ticket counts by status for dashboard. * Get ticket counts by status for dashboard.
*/ */
public function getStatusCounts(): object public function getStatusCounts(): array
{ {
$db = $this->getDatabase(); $db = $this->getDatabase();
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select([$db->quoteName('status'), 'COUNT(*) AS ' . $db->quoteName('cnt')]) ->select([
->from($db->quoteName('#__mokowaas_tickets')) $db->quoteName('s.id'),
->group($db->quoteName('status')) $db->quoteName('s.title'),
$db->quoteName('s.alias'),
$db->quoteName('s.color'),
$db->quoteName('s.is_closed'),
'COUNT(' . $db->quoteName('t.id') . ') AS ' . $db->quoteName('cnt'),
])
->from($db->quoteName('#__mokosuite_ticket_statuses', 's'))
->leftJoin($db->quoteName('#__mokosuite_tickets', 't') . ' ON t.status_id = s.id')
->group($db->quoteName('s.id'))
->order($db->quoteName('s.ordering') . ' ASC')
); );
$rows = $db->loadObjectList('status') ?: [];
return (object) [ return $db->loadObjectList() ?: [];
'open' => (int) ($rows['open']->cnt ?? 0),
'in_progress' => (int) ($rows['in_progress']->cnt ?? 0),
'waiting' => (int) ($rows['waiting']->cnt ?? 0),
'resolved' => (int) ($rows['resolved']->cnt ?? 0),
'closed' => (int) ($rows['closed']->cnt ?? 0),
'total' => array_sum(array_map(fn($r) => (int) $r->cnt, $rows)),
];
} }
/** /**
@@ -372,10 +730,11 @@ class TicketsModel extends BaseDatabaseModel
$now = Factory::getDate()->toSql(); $now = Factory::getDate()->toSql();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('subject'), $db->quoteName('priority'), ->select(['t.' . $db->quoteName('id'), $db->quoteName('t.subject'), $db->quoteName('t.priority'),
$db->quoteName('sla_response_due'), $db->quoteName('sla_resolution_due'), $db->quoteName('sla_responded')]) $db->quoteName('t.sla_response_due'), $db->quoteName('t.sla_resolution_due'), $db->quoteName('t.sla_responded')])
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets', 't'))
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') ->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
->where('(' . $db->quoteName('s.is_closed') . ' = 0 OR ' . $db->quoteName('s.is_closed') . ' IS NULL)')
->where('((' . $db->quoteName('sla_response_due') . ' < ' . $db->quote($now) . ' AND ' . $db->quoteName('sla_responded') . ' = 0)' ->where('((' . $db->quoteName('sla_response_due') . ' < ' . $db->quote($now) . ' AND ' . $db->quoteName('sla_responded') . ' = 0)'
. ' OR ' . $db->quoteName('sla_resolution_due') . ' < ' . $db->quote($now) . ')') . ' OR ' . $db->quoteName('sla_resolution_due') . ' < ' . $db->quote($now) . ')')
->order($db->quoteName('sla_resolution_due') . ' ASC'); ->order($db->quoteName('sla_resolution_due') . ' ASC');
@@ -403,7 +762,7 @@ class TicketsModel extends BaseDatabaseModel
// Load enabled rules for this event // Load enabled rules for this event
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation')) ->from($db->quoteName('#__mokosuite_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event)) ->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1') ->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC'); ->order($db->quoteName('ordering') . ' ASC');
@@ -439,7 +798,7 @@ class TicketsModel extends BaseDatabaseModel
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
\Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas'); \Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
} }
} }
@@ -454,7 +813,7 @@ class TicketsModel extends BaseDatabaseModel
// Load scheduled rules // Load scheduled rules
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation')) ->from($db->quoteName('#__mokosuite_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled')) ->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled'))
->where($db->quoteName('enabled') . ' = 1') ->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC'); ->order($db->quoteName('ordering') . ' ASC');
@@ -469,7 +828,7 @@ class TicketsModel extends BaseDatabaseModel
// Load all non-closed tickets // Load all non-closed tickets
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('status') . ' != ' . $db->quote('closed')); ->where($db->quoteName('status') . ' != ' . $db->quote('closed'));
$db->setQuery($query); $db->setQuery($query);
$tickets = $db->loadObjectList() ?: []; $tickets = $db->loadObjectList() ?: [];
@@ -566,7 +925,7 @@ class TicketsModel extends BaseDatabaseModel
case 'set_priority': case 'set_priority':
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets')) ->update($db->quoteName('#__mokosuite_tickets'))
->set($db->quoteName('priority') . ' = ' . $db->quote($value)) ->set($db->quoteName('priority') . ' = ' . $db->quote($value))
->set($db->quoteName('modified') . ' = ' . $db->quote($now)) ->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId) ->where($db->quoteName('id') . ' = ' . $ticketId)
@@ -576,7 +935,7 @@ class TicketsModel extends BaseDatabaseModel
case 'assign': case 'assign':
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets')) ->update($db->quoteName('#__mokosuite_tickets'))
->set($db->quoteName('assigned_to') . ' = ' . (int) $value) ->set($db->quoteName('assigned_to') . ' = ' . (int) $value)
->set($db->quoteName('modified') . ' = ' . $db->quote($now)) ->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId) ->where($db->quoteName('id') . ' = ' . $ticketId)
@@ -591,7 +950,7 @@ class TicketsModel extends BaseDatabaseModel
'is_internal' => 1, 'is_internal' => 1,
'created' => $now, 'created' => $now,
]; ];
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id'); $db->insertObject('#__mokosuite_ticket_replies', $reply, 'id');
break; break;
case 'send_email': case 'send_email':
@@ -611,7 +970,7 @@ class TicketsModel extends BaseDatabaseModel
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
\Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas'); \Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
} }
} }
break; break;
@@ -629,7 +988,7 @@ class TicketsModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('id')) ->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId) ->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1') ->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1')
@@ -649,7 +1008,7 @@ class TicketsModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('COUNT(*)') ->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_tickets')) ->from($db->quoteName('#__mokosuite_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId) ->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
); );
@@ -684,7 +1043,7 @@ class TicketsModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation')) ->from($db->quoteName('#__mokosuite_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event)) ->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1') ->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC'); ->order($db->quoteName('ordering') . ' ASC');
@@ -721,7 +1080,7 @@ class TicketsModel extends BaseDatabaseModel
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas'); \Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
} }
} }
@@ -734,7 +1093,7 @@ class TicketsModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation')) ->from($db->quoteName('#__mokosuite_ticket_automation'))
->order($db->quoteName('ordering') . ' ASC') ->order($db->quoteName('ordering') . ' ASC')
); );
@@ -781,7 +1140,7 @@ class TicketsModel extends BaseDatabaseModel
try try
{ {
// Status mapping: ATS → MokoWaaS // Status mapping: ATS → MokoSuite
$statusMap = [ $statusMap = [
'O' => 'open', // Open 'O' => 'open', // Open
'P' => 'in_progress', // Pending (staff action needed) 'P' => 'in_progress', // Pending (staff action needed)
@@ -815,7 +1174,7 @@ class TicketsModel extends BaseDatabaseModel
$exists = $db->setQuery( $exists = $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('COUNT(*)') ->select('COUNT(*)')
->from('#__mokowaas_ticket_canned') ->from('#__mokosuite_ticket_canned')
->where($db->quoteName('title') . ' = ' . $db->quote($c->title)) ->where($db->quoteName('title') . ' = ' . $db->quote($c->title))
)->loadResult(); )->loadResult();
@@ -830,7 +1189,7 @@ class TicketsModel extends BaseDatabaseModel
'category_id' => null, 'category_id' => null,
'ordering' => (int) ($c->ordering ?? 0), 'ordering' => (int) ($c->ordering ?? 0),
]; ];
$db->insertObject('#__mokowaas_ticket_canned', $row, 'id'); $db->insertObject('#__mokosuite_ticket_canned', $row, 'id');
$results['canned']++; $results['canned']++;
} }
@@ -838,7 +1197,7 @@ class TicketsModel extends BaseDatabaseModel
$db->setQuery('SELECT * FROM #__ats_tickets ORDER BY id'); $db->setQuery('SELECT * FROM #__ats_tickets ORDER BY id');
$atsTickets = $db->loadObjectList() ?: []; $atsTickets = $db->loadObjectList() ?: [];
$ticketIdMap = []; // ATS id → MokoWaaS id $ticketIdMap = []; // ATS id → MokoSuite id
foreach ($atsTickets as $t) foreach ($atsTickets as $t)
{ {
@@ -846,7 +1205,7 @@ class TicketsModel extends BaseDatabaseModel
$exists = $db->setQuery( $exists = $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('COUNT(*)') ->select('COUNT(*)')
->from('#__mokowaas_tickets') ->from('#__mokosuite_tickets')
->where($db->quoteName('subject') . ' = ' . $db->quote($t->title)) ->where($db->quoteName('subject') . ' = ' . $db->quote($t->title))
->where($db->quoteName('created_by') . ' = ' . (int) $t->created_by) ->where($db->quoteName('created_by') . ' = ' . (int) $t->created_by)
)->loadResult(); )->loadResult();
@@ -874,7 +1233,7 @@ class TicketsModel extends BaseDatabaseModel
'sla_responded' => 1, 'sla_responded' => 1,
]; ];
$db->insertObject('#__mokowaas_tickets', $row, 'id'); $db->insertObject('#__mokosuite_tickets', $row, 'id');
$ticketIdMap[(int) $t->id] = (int) $row->id; $ticketIdMap[(int) $t->id] = (int) $row->id;
$results['tickets']++; $results['tickets']++;
} }
@@ -899,7 +1258,7 @@ class TicketsModel extends BaseDatabaseModel
$body = strip_tags($p->content_html ?? ''); $body = strip_tags($p->content_html ?? '');
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->update('#__mokowaas_tickets') ->update('#__mokosuite_tickets')
->set($db->quoteName('body') . ' = ' . $db->quote($body)) ->set($db->quoteName('body') . ' = ' . $db->quote($body))
->where($db->quoteName('id') . ' = ' . $newTicketId) ->where($db->quoteName('id') . ' = ' . $newTicketId)
)->execute(); )->execute();
@@ -915,7 +1274,7 @@ class TicketsModel extends BaseDatabaseModel
'created' => $p->created ?: Factory::getDate()->toSql(), 'created' => $p->created ?: Factory::getDate()->toSql(),
]; ];
$db->insertObject('#__mokowaas_ticket_replies', $row, 'id'); $db->insertObject('#__mokosuite_ticket_replies', $row, 'id');
$results['replies']++; $results['replies']++;
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Model; namespace Moko\Component\MokoSuite\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -23,7 +23,7 @@ class WaflogModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokowaas_waf_log')); ->from($db->quoteName('#__mokosuite_waf_log'));
if (!empty($filters['rule'])) if (!empty($filters['rule']))
{ {
@@ -69,7 +69,7 @@ class WaflogModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('COUNT(*)') ->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_waf_log')); ->from($db->quoteName('#__mokosuite_waf_log'));
if (!empty($filters['rule'])) if (!empty($filters['rule']))
{ {
@@ -95,7 +95,7 @@ class WaflogModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')]) ->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
->from($db->quoteName('#__mokowaas_waf_log')) ->from($db->quoteName('#__mokosuite_waf_log'))
->group($db->quoteName('rule')) ->group($db->quoteName('rule'))
->order($db->quoteName('cnt') . ' DESC') ->order($db->quoteName('cnt') . ' DESC')
); );
@@ -113,7 +113,7 @@ class WaflogModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'), ->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'),
'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')]) 'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')])
->from($db->quoteName('#__mokowaas_waf_log')) ->from($db->quoteName('#__mokosuite_waf_log'))
->group($db->quoteName('ip')) ->group($db->quoteName('ip'))
->order($db->quoteName('cnt') . ' DESC') ->order($db->quoteName('cnt') . ' DESC')
->setLimit($limit) ->setLimit($limit)
@@ -131,7 +131,7 @@ class WaflogModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->select('DISTINCT ' . $db->quoteName('rule')) ->select('DISTINCT ' . $db->quoteName('rule'))
->from($db->quoteName('#__mokowaas_waf_log')) ->from($db->quoteName('#__mokosuite_waf_log'))
->order($db->quoteName('rule') . ' ASC') ->order($db->quoteName('rule') . ' ASC')
); );
@@ -150,7 +150,7 @@ class WaflogModel extends BaseDatabaseModel
$db->setQuery( $db->setQuery(
$db->getQuery(true) $db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log')) ->delete($db->quoteName('#__mokosuite_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff)) ->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute(); )->execute();
@@ -176,7 +176,7 @@ class WaflogModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')); ->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query); $db->setQuery($query);
@@ -200,7 +200,7 @@ class WaflogModel extends BaseDatabaseModel
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute(); )->execute();
@@ -0,0 +1,177 @@
<?php
/**
* @package MokoSuite
* @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuite\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_mokosuite/attachments';
private const ALLOWED_EXTENSIONS = [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg',
'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);
}
$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) {
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, 'mokosuite');
continue;
}
// Validate size
if ($files['size'][$i] > self::MAX_FILE_SIZE) {
Log::add("Attachment rejected: file too large ({$files['size'][$i]} bytes)", Log::WARNING, 'mokosuite');
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, 'mokosuite');
continue;
}
$record = (object) [
'ticket_id' => $ticketId,
'reply_id' => $replyId,
'filename' => $originalName,
'filepath' => $ticketId . '/' . $storedName,
'filesize' => $files['size'][$i],
'mimetype' => $files['type'][$i],
'uploaded_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuite_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('#__mokosuite_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
{
return self::STORAGE_DIR . '/' . $attachment->filepath;
}
/**
* Delete an attachment (file + DB record).
*/
public static function delete(int $attachmentId): bool
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__mokosuite_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('#__mokosuite_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';
}
}
@@ -0,0 +1,241 @@
<?php
/**
* @package MokoSuite
* @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuite\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, 'mokosuite');
}
}
/**
* 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('#__mokosuite_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) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuite_tickets')} SET status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
}
break;
case 'set_priority':
if ($ticketId) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuite_tickets')} SET priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
}
break;
case 'assign':
if ($ticketId) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuite_tickets')} SET assigned_to = {$db->quote($value)}, 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('#__mokosuite_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) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuite_tickets')} SET status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
}
break;
case 'create_ticket':
self::createTicketFromAutomation($rule, $context, $value);
break;
}
}
catch (\Throwable $e)
{
Log::add("Automation action {$type} failed: " . $e->getMessage(), Log::WARNING, 'mokosuite');
}
}
}
/**
* Create a ticket from automation (with behavior: append/always_new/skip_if_open).
*/
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
$query = $db->getQuery(true)
->select('id')
->from('#__mokosuite_tickets')
->where('created_by = ' . $userId)
->where("status NOT IN ('closed', 'resolved')");
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('#__mokosuite_ticket_replies', $reply);
return;
}
}
// Create new ticket
$ticket = (object) [
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
'body' => $context['body'] ?? '',
'status' => 'open',
'priority' => $context['priority'] ?? 'normal',
'category_id' => $catId ?: null,
'created_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuite_tickets', $ticket, 'id');
}
}
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\Service; namespace Moko\Component\MokoSuite\Administrator\Service;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -67,13 +67,16 @@ class NotificationService
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
// Push notification via ntfy
self::pushNtfy($event, $ticket, $subject);
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
@@ -207,7 +210,7 @@ class NotificationService
{ {
$siteName = Factory::getConfig()->get('sitename', 'Support'); $siteName = Factory::getConfig()->get('sitename', 'Support');
$siteUrl = rtrim(Uri::root(), '/'); $siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/index.php?option=com_mokowaas&view=ticket&id=' . $ticket->id; $ticketUrl = $siteUrl . '/index.php?option=com_mokosuite&view=ticket&id=' . $ticket->id;
$lines = []; $lines = [];
$lines[] = $siteName . ' Support'; $lines[] = $siteName . ' Support';
@@ -273,7 +276,7 @@ class NotificationService
$lines[] = 'View ticket: ' . $ticketUrl; $lines[] = 'View ticket: ' . $ticketUrl;
$lines[] = ''; $lines[] = '';
$lines[] = '-- '; $lines[] = '-- ';
$lines[] = $siteName . ' | Powered by MokoWaaS'; $lines[] = $siteName . ' | Powered by MokoSuite';
return implode("\n", $lines); return implode("\n", $lines);
} }
@@ -318,7 +321,7 @@ class NotificationService
$db->getQuery(true) $db->getQuery(true)
->select($db->quoteName('params')) ->select($db->quoteName('params'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
); );
@@ -332,6 +335,159 @@ class NotificationService
} }
} }
// ==================================================================
// Ntfy Push Notifications (#205)
// ==================================================================
/**
* Send a push notification via ntfy for ticket events.
*/
private static function pushNtfy(string $event, object $ticket, string $title): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_topic'] ?? 'mokosuite-tickets';
$ntfyToken = $config['ntfy_token'] ?? '';
$tagMap = [
'ticket_created' => 'ticket,new',
'ticket_replied' => 'speech_balloon',
'status_changed' => 'arrows_counterclockwise',
'ticket_assigned' => 'bust_in_silhouette',
];
$priorityMap = [
'ticket_created' => '4',
'ticket_replied' => '3',
'status_changed' => '3',
'ticket_assigned' => '3',
];
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/administrator/index.php?option=com_mokosuite&view=ticket&id=' . ($ticket->id ?? 0);
$message = self::buildNtfyMessage($event, $ticket);
$headers = [
'Title: ' . $title,
'Priority: ' . ($priorityMap[$event] ?? '3'),
'Tags: ' . ($tagMap[$event] ?? 'ticket'),
'Click: ' . $ticketUrl,
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300)
{
Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuite');
}
}
catch (\Throwable $e)
{
Log::add('Ntfy push error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
}
}
/**
* Build a short ntfy message body for ticket events.
*/
private static function buildNtfyMessage(string $event, object $ticket): string
{
$subject = $ticket->subject ?? 'Ticket #' . ($ticket->id ?? '?');
switch ($event)
{
case 'ticket_created':
$priority = ucfirst($ticket->priority ?? 'normal');
return "New ticket: {$subject}\nPriority: {$priority}";
case 'ticket_replied':
return "Reply on: {$subject}";
case 'status_changed':
$status = ucwords(str_replace('_', ' ', $ticket->status ?? ''));
return "Status → {$status}: {$subject}";
case 'ticket_assigned':
return "Assigned to you: {$subject}";
default:
return $subject;
}
}
/**
* Send a push notification via ntfy for security events.
*/
public static function pushNtfySecurity(string $event, string $title, string $body): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_security_topic'] ?? $config['ntfy_topic'] ?? 'mokosuite-security';
$ntfyToken = $config['ntfy_token'] ?? '';
$headers = [
'Title: [Security] ' . $title,
'Priority: 5',
'Tags: warning,shield',
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
}
catch (\Throwable $e)
{
Log::add('Ntfy security push error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
}
}
// ================================================================== // ==================================================================
// Security Event Notifications (#131) // Security Event Notifications (#131)
// ================================================================== // ==================================================================
@@ -386,7 +542,7 @@ class NotificationService
$body, $body,
'', '',
'-- ', '-- ',
$siteName . ' | MokoWaaS Security', $siteName . ' | MokoSuite Security',
]; ];
$mailer = Factory::getMailer(); $mailer = Factory::getMailer();
@@ -404,13 +560,16 @@ class NotificationService
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
// Also push via ntfy
self::pushNtfySecurity($event, $subject, $body);
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokowaas'); Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
} }
} }
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Automation; namespace Moko\Component\MokoSuite\Administrator\View\Automation;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
public function display($tpl = null) public function display($tpl = null)
{ {
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\TicketsModel();
$this->rules = $model->getAutomationRules(); $this->rules = $model->getAutomationRules();
ToolbarHelper::title('Automation Rules', 'cogs'); ToolbarHelper::title('Automation Rules', 'cogs');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Canned; namespace Moko\Component\MokoSuite\Administrator\View\Canned;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,17 +16,17 @@ class HtmlView extends BaseHtmlView
{ {
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface'); $db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_canned ORDER BY ordering ASC'); $db->setQuery('SELECT * FROM #__mokosuite_ticket_canned ORDER BY ordering ASC');
$this->responses = $db->loadObjectList() ?: []; $this->responses = $db->loadObjectList() ?: [];
$db->setQuery('SELECT id, title FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering'); $db->setQuery('SELECT id, title FROM #__mokosuite_ticket_categories WHERE published = 1 ORDER BY ordering');
$this->categories = $db->loadObjectList() ?: []; $this->categories = $db->loadObjectList() ?: [];
ToolbarHelper::title('Canned Responses', 'comment'); ToolbarHelper::title('Canned Responses', 'comment');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Categories; namespace Moko\Component\MokoSuite\Administrator\View\Categories;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ class HtmlView extends BaseHtmlView
{ {
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface'); $db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_categories ORDER BY ordering ASC'); $db->setQuery('SELECT * FROM #__mokosuite_ticket_categories ORDER BY ordering ASC');
$this->categories = $db->loadObjectList() ?: []; $this->categories = $db->loadObjectList() ?: [];
// Get admin users for auto-assign dropdown // Get admin users for auto-assign dropdown
@@ -31,10 +31,10 @@ class HtmlView extends BaseHtmlView
$this->users = $db->loadObjectList() ?: []; $this->users = $db->loadObjectList() ?: [];
ToolbarHelper::title('Ticket Categories', 'folder'); ToolbarHelper::title('Ticket Categories', 'folder');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Cleanup; namespace Moko\Component\MokoSuite\Administrator\View\Cleanup;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
public function display($tpl = null) public function display($tpl = null)
{ {
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->dirs = $model->getCleanupInfo(); $this->dirs = $model->getCleanupInfo();
ToolbarHelper::title('Cache &amp; Temp Cleanup', 'trash'); ToolbarHelper::title('Cache &amp; Temp Cleanup', 'trash');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\View\Dashboard; namespace Moko\Component\MokoSuite\Administrator\View\Dashboard;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -44,7 +44,7 @@ class HtmlView extends BaseHtmlView
// Check for importable Akeeba data // Check for importable Akeeba data
try try
{ {
$importModel = new \Moko\Component\MokoWaaS\Administrator\Model\ImportModel(); $importModel = new \Moko\Component\MokoSuite\Administrator\Model\ImportModel();
$this->adminToolsAvailable = $importModel->checkAdminToolsAvailable(); $this->adminToolsAvailable = $importModel->checkAdminToolsAvailable();
$this->atsAvailable = $importModel->checkAtsAvailable(); $this->atsAvailable = $importModel->checkAtsAvailable();
} }
@@ -57,21 +57,21 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
$wa->registerAndUseScript('com_mokowaas.dashboard', 'com_mokowaas/dashboard.js', [], ['defer' => true]); $wa->registerAndUseScript('com_mokosuite.dashboard', 'com_mokosuite/dashboard.js', [], ['defer' => true]);
parent::display($tpl); parent::display($tpl);
} }
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title(Text::_('COM_MOKOWAAS_DASHBOARD_TITLE'), 'cogs'); ToolbarHelper::title(Text::_('COM_MOKOSUITE_DASHBOARD_TITLE'), 'cogs');
$user = Factory::getApplication()->getIdentity(); $user = Factory::getApplication()->getIdentity();
if ($user->authorise('core.admin', 'com_mokowaas')) if ($user->authorise('core.admin', 'com_mokosuite'))
{ {
ToolbarHelper::preferences('com_mokowaas'); ToolbarHelper::preferences('com_mokosuite');
} }
} }
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Database; namespace Moko\Component\MokoSuite\Administrator\View\Database;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
public function display($tpl = null) public function display($tpl = null)
{ {
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
$this->tableData = $model->getTableStatus(); $this->tableData = $model->getTableStatus();
ToolbarHelper::title('Database Tools', 'database'); ToolbarHelper::title('Database Tools', 'database');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -0,0 +1,40 @@
<?php
namespace Moko\Component\MokoSuite\Administrator\View\ErpReports;
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 $activeTab = 'sales';
protected $salesData = [];
protected $topCustomers = [];
protected $topProducts = [];
protected $pipelineData = [];
protected $agingData = [];
protected $dateFrom;
protected $dateTo;
public function display($tpl = null)
{
$model = $this->getModel();
$input = Factory::getApplication()->getInput();
$this->activeTab = $input->get('tab', 'sales', 'CMD');
$this->dateFrom = $input->get('from', date('Y-01-01'), 'STRING');
$this->dateTo = $input->get('to', date('Y-m-d'), 'STRING');
$this->salesData = $model->getSalesReport($this->dateFrom, $this->dateTo);
$this->topCustomers = $model->getTopCustomers($this->dateFrom, $this->dateTo);
$this->topProducts = $model->getTopProducts($this->dateFrom, $this->dateTo);
$this->pipelineData = $model->getPipelineReport($this->dateFrom, $this->dateTo);
$this->agingData = $model->getAgingReceivables();
ToolbarHelper::title('ERP Reports', 'icon-chart-bar');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuite.erp', 'com_mokosuite/erp.css');
$wa->registerAndUseScript('com_mokosuite.erp-dashboard', 'com_mokosuite/erp-dashboard.js', [], ['defer' => true]);
Factory::getApplication()->getDocument()->addScriptOptions('mokosuite.erp', ['revenueChart' => $this->salesData]);
parent::display($tpl);
}
}
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\View\Extensions; namespace Moko\Component\MokoSuite\Administrator\View\Extensions;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -28,14 +28,14 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title(Text::_('COM_MOKOWAAS_EXTENSIONS_TITLE'), 'puzzle-piece'); ToolbarHelper::title(Text::_('COM_MOKOSUITE_EXTENSIONS_TITLE'), 'puzzle-piece');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
} }
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\View\Htaccess; namespace Moko\Component\MokoSuite\Administrator\View\Htaccess;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -34,14 +34,14 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title(Text::_('COM_MOKOWAAS_HTACCESS_TITLE'), 'file-code'); ToolbarHelper::title(Text::_('COM_MOKOSUITE_HTACCESS_TITLE'), 'file-code');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
} }
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Privacy; namespace Moko\Component\MokoSuite\Administrator\View\Privacy;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ class HtmlView extends BaseHtmlView
public function display($tpl = null) public function display($tpl = null)
{ {
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', ''); $filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
$this->requests = $model->getDataRequests($filterStatus); $this->requests = $model->getDataRequests($filterStatus);
@@ -26,7 +26,7 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -34,6 +34,6 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title('Privacy Guard', 'lock'); ToolbarHelper::title('Privacy Guard', 'lock');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
} }
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\View\Ticket; namespace Moko\Component\MokoSuite\Administrator\View\Ticket;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,6 +19,11 @@ class HtmlView extends BaseHtmlView
{ {
protected $ticket; protected $ticket;
protected $cannedResponses = []; protected $cannedResponses = [];
protected $statuses = [];
protected $priorities = [];
protected $customFields = [];
protected $fieldValues = [];
protected $attachments = [];
public function display($tpl = null) public function display($tpl = null)
{ {
@@ -27,11 +32,25 @@ class HtmlView extends BaseHtmlView
$this->ticket = $model->getTicket($id); $this->ticket = $model->getTicket($id);
$this->cannedResponses = $model->getCannedResponses((int) ($this->ticket->category_id ?? 0)); $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\MokoSuite\Administrator\Service\AttachmentService::getForTicket($id);
if (!$this->ticket) if (!$this->ticket)
{ {
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error'); Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
Factory::getApplication()->redirect('index.php?option=com_mokowaas&view=tickets'); Factory::getApplication()->redirect('index.php?option=com_mokosuite&view=tickets');
return; return;
} }
@@ -39,7 +58,7 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -48,6 +67,6 @@ class HtmlView extends BaseHtmlView
{ {
$title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket'; $title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket';
ToolbarHelper::title($title, 'headphones'); ToolbarHelper::title($title, 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
} }
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Administrator\View\Tickets; namespace Moko\Component\MokoSuite\Administrator\View\Tickets;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -22,6 +22,9 @@ class HtmlView extends BaseHtmlView
protected $statusCounts; protected $statusCounts;
protected $overdue = []; protected $overdue = [];
protected $atsAvailable = null; protected $atsAvailable = null;
protected $contacts = [];
protected $statuses = [];
protected $priorities = [];
public function display($tpl = null) public function display($tpl = null)
{ {
@@ -29,28 +32,32 @@ class HtmlView extends BaseHtmlView
$app = Factory::getApplication(); $app = Factory::getApplication();
$filters = [ $filters = [
'status' => $app->getInput()->getString('filter_status', ''), 'status_id' => $app->getInput()->getInt('filter_status', 0),
'priority' => $app->getInput()->getString('filter_priority', ''), 'priority_id' => $app->getInput()->getInt('filter_priority', 0),
'category_id' => $app->getInput()->getInt('filter_category', 0), 'category_id' => $app->getInput()->getInt('filter_category', 0),
'contact_id' => $app->getInput()->getInt('filter_contact', 0),
]; ];
$this->tickets = $model->getTickets($filters); $this->tickets = $model->getTickets($filters);
$this->categories = $model->getCategories(); $this->categories = $model->getCategories();
$this->statuses = $model->getStatuses();
$this->priorities = $model->getPriorities();
$this->statusCounts = $model->getStatusCounts(); $this->statusCounts = $model->getStatusCounts();
$this->overdue = $model->getOverdueTickets(); $this->overdue = $model->getOverdueTickets();
$this->atsAvailable = $model->checkAtsAvailable(); $this->atsAvailable = $model->checkAtsAvailable();
$this->contacts = $model->getContacts();
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title(Text::_('COM_MOKOWAAS_TICKETS_TITLE'), 'headphones'); ToolbarHelper::title(Text::_('COM_MOKOSUITE_TICKETS_TITLE'), 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
} }
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace Moko\Component\MokoWaaS\Administrator\View\Waflog; namespace Moko\Component\MokoSuite\Administrator\View\Waflog;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -18,7 +18,7 @@ class HtmlView extends BaseHtmlView
public function display($tpl = null) public function display($tpl = null)
{ {
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel(); $model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$this->filters = [ $this->filters = [
@@ -42,7 +42,7 @@ class HtmlView extends BaseHtmlView
$this->addToolbar(); $this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); $wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
parent::display($tpl); parent::display($tpl);
} }
@@ -50,6 +50,6 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title('WAF Log Viewer', 'shield-alt'); ToolbarHelper::title('WAF Log Viewer', 'shield-alt');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
} }
} }
@@ -0,0 +1,297 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$rules = $this->rules;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveAutomation&format=json');
$deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteAutomation&format=json');
$toggleUrl = Route::_('index.php?option=com_mokosuite&task=display.toggleAutomation&format=json');
$reorderUrl = Route::_('index.php?option=com_mokosuite&task=display.reorderAutomation&format=json');
$triggerLabels = [
'ticket_created' => 'On Ticket Created',
'ticket_replied' => 'On Reply',
'status_changed' => 'On Status Change',
'ticket_assigned' => 'On Assignment',
'user_login' => 'On User Login',
'user_register' => 'On User Register',
'user_login_failed' => 'On Failed Login',
'content_save' => 'On Article Save',
'extension_install' => 'On Extension Install',
'scheduled' => 'Scheduled (Cron)',
];
$conditionFields = ['status', 'priority', 'category_id', 'assigned_to', 'sla_responded', 'age_hours'];
$conditionOps = ['eq' => '=', 'neq' => '≠', 'gt' => '>', 'lt' => '<', 'in' => 'in', 'not_in' => 'not in'];
$actionTypes = ['set_status', 'set_priority', 'assign', 'add_note', 'send_email', 'send_ntfy', 'close'];
?>
<div id="mokosuite-automation">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($rules); ?> Automation Rules</h4>
<button type="button" class="btn btn-primary btn-sm" onclick="openRuleModal(0)">
<span class="icon-plus"></span> Add Rule
</button>
</div>
<div id="rules-list">
<?php foreach ($rules as $r): ?>
<?php $conditions = json_decode($r->conditions, true) ?: []; $actions = json_decode($r->actions, true) ?: []; ?>
<div class="card mb-2 rule-card <?php echo !$r->enabled ? 'opacity-50' : ''; ?>" data-id="<?php echo $r->id; ?>" draggable="true">
<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="openRuleModal(<?php echo $r->id; ?>)">
<div class="d-flex align-items-center gap-2">
<span class="icon-menu text-muted" style="cursor:grab;"></span>
<div class="form-check form-switch" onclick="event.stopPropagation();">
<input type="checkbox" class="form-check-input rule-toggle" data-id="<?php echo $r->id; ?>" <?php echo $r->enabled ? 'checked' : ''; ?>>
</div>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<span class="badge bg-secondary"><?php echo $triggerLabels[$r->trigger_event] ?? $r->trigger_event; ?></span>
</div>
<div class="small text-muted mt-1 ms-4">
<?php if (!empty($conditions)): ?>
<span class="text-primary">IF</span>
<?php foreach ($conditions as $i => $c): ?>
<?php echo $i > 0 ? ' AND ' : ''; ?><code><?php echo htmlspecialchars($c['field'] ?? ''); ?></code> <?php echo $conditionOps[$c['op'] ?? ''] ?? $c['op'] ?? ''; ?> <em><?php echo htmlspecialchars($c['value'] ?? ''); ?></em>
<?php endforeach; ?>
<?php endif; ?>
<span class="text-success ms-1">THEN</span>
<?php foreach ($actions as $a): ?>
<code><?php echo htmlspecialchars($a['type'] ?? ''); ?></code><?php if (!empty($a['value'])): ?>=<em><?php echo htmlspecialchars(mb_substr($a['value'], 0, 30)); ?></em><?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-rule" data-id="<?php echo $r->id; ?>" onclick="event.stopPropagation();">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($rules)): ?>
<div class="alert alert-info">No automation rules. Click "Add Rule" to create one.</div>
<?php endif; ?>
</div>
</div>
<!-- Rule Modal -->
<div class="modal fade" id="ruleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5 id="ruleModalTitle">Add Automation Rule</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<input type="hidden" id="rule-id" value="0">
<div class="row mb-3">
<div class="col-5">
<label class="form-label">Title</label>
<input type="text" id="rule-title" class="form-control" required>
</div>
<div class="col-4">
<label class="form-label">Trigger</label>
<select id="rule-trigger" class="form-select">
<?php foreach ($triggerLabels as $k => $v): ?><option value="<?php echo $k; ?>"><?php echo $v; ?></option><?php endforeach; ?>
</select>
</div>
<div class="col-3">
<label class="form-label">Behavior</label>
<select id="rule-behavior" class="form-select">
<option value="append">Append to existing</option>
<option value="always_new">Always new ticket</option>
<option value="skip_if_open">Skip if open</option>
</select>
</div>
</div>
<label class="form-label">Conditions <small class="text-muted">(all must match)</small></label>
<div id="conditions-builder" class="mb-3"></div>
<button type="button" class="btn btn-sm btn-outline-secondary mb-3" onclick="addConditionRow()"><span class="icon-plus"></span> Add Condition</button>
<label class="form-label">Actions</label>
<div id="actions-builder" class="mb-3"></div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="addActionRow()"><span class="icon-plus"></span> Add Action</button>
</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-rule"><span class="icon-save"></span> Save Rule</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var tokenKey = '<?php echo $token; ?>';
var condFields = <?php echo json_encode($conditionFields); ?>;
var condOps = <?php echo json_encode($conditionOps); ?>;
var actTypes = <?php echo json_encode($actionTypes); ?>;
// Rule data store for editing
var ruleData = {};
<?php foreach ($rules as $r): ?>
ruleData[<?php echo $r->id; ?>] = {
title: <?php echo json_encode($r->title); ?>,
trigger_event: <?php echo json_encode($r->trigger_event); ?>,
behavior: <?php echo json_encode($r->behavior ?? 'append'); ?>,
conditions: <?php echo $r->conditions ?: '[]'; ?>,
actions: <?php echo $r->actions ?: '[]'; ?>
};
<?php endforeach; ?>
// ── Builder helpers ─────────────────────────────────────────
function makeSelect(cls, options, selected) {
var sel = document.createElement('select');
sel.className = 'form-select ' + cls;
options.forEach(function(o) {
var opt = document.createElement('option');
opt.value = o.value;
opt.textContent = o.label;
if (o.value === selected) opt.selected = true;
sel.appendChild(opt);
});
return sel;
}
function makeRemoveBtn() {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-outline-danger';
btn.innerHTML = '<span class="icon-minus"></span>';
btn.addEventListener('click', function() { this.parentNode.remove(); });
return btn;
}
window.addConditionRow = function(field, op, value) {
var div = document.createElement('div');
div.className = 'input-group input-group-sm mb-1';
div.appendChild(makeSelect('cond-field', condFields.map(function(f){return {value:f, label:f}}), field));
div.appendChild(makeSelect('cond-op', Object.keys(condOps).map(function(k){return {value:k, label:condOps[k]}}), op));
var inp = document.createElement('input');
inp.type = 'text'; inp.className = 'form-control cond-value'; inp.placeholder = 'value'; inp.value = value || '';
div.appendChild(inp);
div.appendChild(makeRemoveBtn());
document.getElementById('conditions-builder').appendChild(div);
};
window.addActionRow = function(type, value) {
var div = document.createElement('div');
div.className = 'input-group input-group-sm mb-1';
div.appendChild(makeSelect('act-type', actTypes.map(function(t){return {value:t, label:t}}), type));
var inp = document.createElement('input');
inp.type = 'text'; inp.className = 'form-control act-value'; inp.placeholder = 'value'; inp.value = value || '';
div.appendChild(inp);
div.appendChild(makeRemoveBtn());
document.getElementById('actions-builder').appendChild(div);
};
// ── Open modal ──────────────────────────────────────────────
window.openRuleModal = function(id) {
document.getElementById('rule-id').value = id;
document.getElementById('conditions-builder').innerHTML = '';
document.getElementById('actions-builder').innerHTML = '';
if (id > 0 && ruleData[id]) {
document.getElementById('ruleModalTitle').textContent = 'Edit Automation Rule';
document.getElementById('rule-title').value = ruleData[id].title;
document.getElementById('rule-trigger').value = ruleData[id].trigger_event;
document.getElementById('rule-behavior').value = ruleData[id].behavior || 'append';
ruleData[id].conditions.forEach(function(c) { addConditionRow(c.field, c.op, c.value); });
ruleData[id].actions.forEach(function(a) { addActionRow(a.type, a.value); });
} else {
document.getElementById('ruleModalTitle').textContent = 'Add Automation Rule';
document.getElementById('rule-title').value = '';
document.getElementById('rule-trigger').value = 'ticket_created';
document.getElementById('rule-behavior').value = 'append';
addConditionRow();
addActionRow();
}
new bootstrap.Modal(document.getElementById('ruleModal')).show();
};
// ── Save rule ───────────────────────────────────────────────
document.getElementById('btn-save-rule').addEventListener('click', function() {
var conditions = [];
document.querySelectorAll('#conditions-builder .input-group').forEach(function(row) {
var f = row.querySelector('.cond-field').value;
var o = row.querySelector('.cond-op').value;
var v = row.querySelector('.cond-value').value;
if (f && v) conditions.push({field:f, op:o, value:v});
});
var actions = [];
document.querySelectorAll('#actions-builder .input-group').forEach(function(row) {
var t = row.querySelector('.act-type').value;
var v = row.querySelector('.act-value').value;
if (t) actions.push({type:t, value:v});
});
var fd = new FormData();
fd.append('id', document.getElementById('rule-id').value);
fd.append('title', document.getElementById('rule-title').value);
fd.append('trigger_event', document.getElementById('rule-trigger').value);
fd.append('behavior', document.getElementById('rule-behavior').value);
fd.append('conditions', JSON.stringify(conditions));
fd.append('actions', JSON.stringify(actions));
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]}); });
});
// ── Toggle ──────────────────────────────────────────────────
document.querySelectorAll('.rule-toggle').forEach(function(cb) {
cb.addEventListener('change', function() {
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append('enabled', this.checked ? '1' : '0');
fd.append(tokenKey, '1');
fetch('<?php echo $toggleUrl; ?>', {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]}); else this.closest('.card').classList.toggle('opacity-50', !this.checked); }.bind(this));
});
});
// ── Delete ──────────────────────────────────────────────────
document.querySelectorAll('.btn-delete-rule').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this rule?')) 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]}); });
});
});
// ── Drag-and-drop reorder ───────────────────────────────────
var list = document.getElementById('rules-list');
var dragCard = null;
list.addEventListener('dragstart', function(e) {
dragCard = e.target.closest('.rule-card');
if (dragCard) dragCard.style.opacity = '0.5';
});
list.addEventListener('dragend', function() { if (dragCard) dragCard.style.opacity = ''; dragCard = null; });
list.addEventListener('dragover', function(e) {
e.preventDefault();
var target = e.target.closest('.rule-card');
if (target && target !== dragCard) {
var rect = target.getBoundingClientRect();
if ((e.clientY - rect.top) > rect.height / 2) target.parentNode.insertBefore(dragCard, target.nextSibling);
else target.parentNode.insertBefore(dragCard, target);
}
});
list.addEventListener('drop', function(e) {
e.preventDefault();
var ids = [];
document.querySelectorAll('.rule-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'}});
});
});
</script>
@@ -0,0 +1,227 @@
<?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_mokosuite&task=display.saveCanned&format=json');
$deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteCanned&format=json');
$reorderUrl = Route::_('index.php?option=com_mokosuite&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="mokosuite-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>
@@ -7,11 +7,12 @@ use Joomla\CMS\Session\Session;
$categories = $this->categories; $categories = $this->categories;
$users = $this->users; $users = $this->users;
$token = Session::getFormToken(); $token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCategory&format=json'); $saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveCategory&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCategory&format=json'); $deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteCategory&format=json');
$reorderUrl = Route::_('index.php?option=com_mokosuite&task=display.reorderCategory&format=json');
?> ?>
<div id="mokowaas-categories"> <div id="mokosuite-categories">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($categories); ?> Categories</h4> <h4><?php echo count($categories); ?> Categories</h4>
<button type="button" class="btn btn-primary btn-sm" id="btn-add-cat"> <button type="button" class="btn btn-primary btn-sm" id="btn-add-cat">
@@ -22,10 +23,11 @@ $deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCategor
<div class="card"> <div class="card">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped mb-0" id="cat-table"> <table class="table table-striped mb-0" id="cat-table">
<thead><tr><th>Title</th><th>SLA Response</th><th>SLA Resolution</th><th>Auto-Assign</th><th>Active</th><th></th></tr></thead> <thead><tr><th style="width:30px"></th><th>Title</th><th>SLA Response</th><th>SLA Resolution</th><th>Auto-Assign</th><th>Active</th><th></th></tr></thead>
<tbody> <tbody>
<?php foreach ($categories as $c): ?> <?php foreach ($categories as $c): ?>
<tr data-id="<?php echo $c->id; ?>"> <tr data-id="<?php echo $c->id; ?>" draggable="true">
<td><span class="icon-menu text-muted" style="cursor:grab;"></span></td>
<td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value="<?php echo htmlspecialchars($c->title); ?>"></td> <td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value="<?php echo htmlspecialchars($c->title); ?>"></td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="<?php echo $c->sla_response_minutes; ?>" style="width:80px"> min</td> <td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="<?php echo $c->sla_response_minutes; ?>" style="width:80px"> min</td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="<?php echo $c->sla_resolution_minutes; ?>" style="width:80px"> min</td> <td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="<?php echo $c->sla_resolution_minutes; ?>" style="width:80px"> min</td>
@@ -122,5 +124,39 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
tr.querySelector('input').focus(); tr.querySelector('input').focus();
}); });
// Drag-and-drop reorder
var tbody = document.querySelector('#cat-table tbody');
var dragRow = null;
tbody.addEventListener('dragstart', function(e) {
dragRow = e.target.closest('tr');
if (dragRow) dragRow.style.opacity = '0.5';
});
tbody.addEventListener('dragend', function() {
if (dragRow) dragRow.style.opacity = '';
dragRow = null;
});
tbody.addEventListener('dragover', function(e) {
e.preventDefault();
var target = e.target.closest('tr');
if (target && target !== dragRow) {
var rect = target.getBoundingClientRect();
if ((e.clientY - rect.top) > rect.height / 2) {
target.parentNode.insertBefore(dragRow, target.nextSibling);
} else {
target.parentNode.insertBefore(dragRow, target);
}
}
});
tbody.addEventListener('drop', function(e) {
e.preventDefault();
var ids = [];
tbody.querySelectorAll('tr[data-id]').forEach(function(r) { if (r.dataset.id !== '0') ids.push(r.dataset.id); });
var fd = new FormData();
fd.append('order', JSON.stringify(ids));
fd.append(token, '1');
fetch('<?php echo $reorderUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}});
});
}); });
</script> </script>
@@ -6,7 +6,7 @@ use Joomla\CMS\Session\Session;
$dirs = $this->dirs; $dirs = $this->dirs;
$token = Session::getFormToken(); $token = Session::getFormToken();
$cleanUrl = Route::_('index.php?option=com_mokowaas&task=display.cleanDirectory&format=json'); $cleanUrl = Route::_('index.php?option=com_mokosuite&task=display.cleanDirectory&format=json');
$dirKeys = ['site_cache', 'admin_cache', 'tmp', 'logs']; $dirKeys = ['site_cache', 'admin_cache', 'tmp', 'logs'];
$totalMb = 0; $totalMb = 0;
@@ -14,7 +14,7 @@ $totalFiles = 0;
foreach ($dirs as $d) { $totalMb += $d->size_mb; $totalFiles += $d->files; } foreach ($dirs as $d) { $totalMb += $d->size_mb; $totalFiles += $d->files; }
?> ?>
<div id="mokowaas-cleanup"> <div id="mokosuite-cleanup">
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalMb, 1); ?> MB</span><small class="text-muted">Total Size</small></div></div> <div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalMb, 1); ?> MB</span><small class="text-muted">Total Size</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalFiles); ?></span><small class="text-muted">Total Files</small></div></div> <div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalFiles); ?></span><small class="text-muted">Total Files</small></div></div>
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
@@ -13,7 +13,7 @@ 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;
/** @var \Moko\Component\MokoWaaS\Administrator\View\Dashboard\HtmlView $this */ /** @var \Moko\Component\MokoSuite\Administrator\View\Dashboard\HtmlView $this */
$siteInfo = $this->siteInfo; $siteInfo = $this->siteInfo;
$plugins = $this->plugins; $plugins = $this->plugins;
@@ -36,37 +36,37 @@ foreach ($plugins as $plugin)
$categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api']; $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
?> ?>
<div id="mokowaas-dashboard"> <div id="mokosuite-dashboard">
<!-- Site Info Bar --> <!-- Site Info Bar -->
<div class="mokowaas-info-bar card mb-4"> <div class="mokosuite-info-bar 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-4">
<div class="mokowaas-info-item"> <div class="mokosuite-info-item">
<span class="mokowaas-info-label"><?php echo Text::_('COM_MOKOWAAS_SITE'); ?></span> <span class="mokosuite-info-label"><?php echo Text::_('COM_MOKOSUITE_SITE'); ?></span>
<span class="mokowaas-info-value fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span> <span class="mokosuite-info-value fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
</div> </div>
<div class="mokowaas-info-item"> <div class="mokosuite-info-item">
<span class="mokowaas-info-label">MokoWaaS</span> <span class="mokosuite-info-label">MokoSuite</span>
<span class="mokowaas-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokowaas_version); ?></span></span> <span class="mokosuite-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokosuite_version); ?></span></span>
</div> </div>
<div class="mokowaas-info-item"> <div class="mokosuite-info-item">
<span class="mokowaas-info-label">Joomla</span> <span class="mokosuite-info-label">Joomla</span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span> <span class="mokosuite-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span>
</div> </div>
<div class="mokowaas-info-item"> <div class="mokosuite-info-item">
<span class="mokowaas-info-label">PHP</span> <span class="mokosuite-info-label">PHP</span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->php_version); ?></span></span> <span class="mokosuite-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->php_version); ?></span></span>
</div> </div>
<div class="mokowaas-info-item"> <div class="mokosuite-info-item">
<span class="mokowaas-info-label"><?php echo Text::_('COM_MOKOWAAS_DATABASE'); ?></span> <span class="mokosuite-info-label"><?php echo Text::_('COM_MOKOSUITE_DATABASE'); ?></span>
<span class="mokowaas-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span></span> <span class="mokosuite-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span></span>
</div> </div>
<?php if ($siteInfo->debug): ?> <?php if ($siteInfo->debug): ?>
<span class="badge bg-warning text-dark"><?php echo Text::_('COM_MOKOWAAS_DEBUG_ON'); ?></span> <span class="badge bg-warning text-dark"><?php echo Text::_('COM_MOKOSUITE_DEBUG_ON'); ?></span>
<?php endif; ?> <?php endif; ?>
<?php if ($siteInfo->offline): ?> <?php if ($siteInfo->offline): ?>
<span class="badge bg-danger"><?php echo Text::_('COM_MOKOWAAS_OFFLINE'); ?></span> <span class="badge bg-danger"><?php echo Text::_('COM_MOKOSUITE_OFFLINE'); ?></span>
<?php endif; ?> <?php endif; ?>
<div class="mokowaas-info-item ms-auto"> <div class="mokosuite-info-item ms-auto">
<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> </div>
@@ -78,15 +78,15 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<div class="d-flex flex-wrap gap-2 mb-4"> <div class="d-flex flex-wrap gap-2 mb-4">
<?php <?php
$extIcons = [ $extIcons = [
'com_mokowaas' => 'icon-cogs', 'com_mokosuite' => 'icon-cogs',
'mod_mokowaas_cpanel' => 'icon-tachometer-alt', 'mod_mokosuite_cpanel' => 'icon-tachometer-alt',
'mod_mokowaas_menu' => 'icon-bars', 'mod_mokosuite_menu' => 'icon-bars',
'mod_mokowaas_cache' => 'icon-bolt', 'mod_mokosuite_cache' => 'icon-bolt',
'mod_mokowaas_categories' => 'icon-folder', 'mod_mokosuite_categories' => 'icon-folder',
]; ];
foreach ($mokoExts as $ext): foreach ($mokoExts as $ext):
$icon = $extIcons[$ext->element] ?? 'icon-puzzle-piece'; $icon = $extIcons[$ext->element] ?? 'icon-puzzle-piece';
$label = str_replace(['mod_mokowaas_', 'com_mokowaas'], ['', 'Component'], $ext->element); $label = str_replace(['mod_mokosuite_', 'com_mokosuite'], ['', 'Component'], $ext->element);
$label = ucfirst($label ?: 'Component'); $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;"> <div class="d-flex align-items-center gap-2 px-3 py-2 rounded border bg-white" style="font-size:0.85rem;">
@@ -102,17 +102,17 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<!-- Akeeba Import Banner --> <!-- Akeeba Import Banner -->
<div class="alert alert-info d-flex flex-wrap align-items-center gap-3 mb-4"> <div class="alert alert-info d-flex flex-wrap align-items-center gap-3 mb-4">
<span class="icon-info-circle" style="font-size:1.25rem"></span> <span class="icon-info-circle" style="font-size:1.25rem"></span>
<strong>Akeeba data detected import into MokoWaaS:</strong> <strong>Akeeba data detected import into MokoSuite:</strong>
<?php if ($adminToolsAvail): ?> <?php if ($adminToolsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-admintools" <button type="button" class="btn btn-sm btn-info" id="btn-import-admintools"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAdminTools&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.importAdminTools&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-shield-alt"></span> Import Admin Tools Settings <span class="icon-shield-alt"></span> Import Admin Tools Settings
</button> </button>
<?php endif; ?> <?php endif; ?>
<?php if ($atsAvail): ?> <?php if ($atsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-ats-dash" <button type="button" class="btn btn-sm btn-info" id="btn-import-ats-dash"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAts&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-headphones"></span> Import Tickets (<?php echo $atsAvail->tickets; ?> tickets) <span class="icon-headphones"></span> Import Tickets (<?php echo $atsAvail->tickets; ?> tickets)
</button> </button>
@@ -123,8 +123,8 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<!-- Quick Actions (large buttons) --> <!-- Quick Actions (large buttons) -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-4 col-xl-3"> <div class="col-6 col-md-4 col-xl-3">
<button type="button" class="btn btn-outline-primary w-100 py-3" id="mokowaas-btn-cache" <button type="button" class="btn btn-outline-primary w-100 py-3" id="mokosuite-btn-cache"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.clearCache&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-bolt d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span> <span class="icon-bolt d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Clear Cache Clear Cache
@@ -137,7 +137,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
</a> </a>
</div> </div>
<div class="col-6 col-md-4 col-xl-3"> <div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3"> <a href="<?php echo Route::_('index.php?option=com_mokosuite&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span> <span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Moko Extensions Moko Extensions
</a> </a>
@@ -192,18 +192,18 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
$catPlugins = $grouped[$catKey]; $catPlugins = $grouped[$catKey];
$first = $catPlugins[0]; $first = $catPlugins[0];
?> ?>
<h3 class="mokowaas-category-heading mb-3"> <h3 class="mokosuite-category-heading mb-3">
<span class="badge <?php echo $this->escape($first->categoryBadge); ?>"><?php echo $this->escape($first->categoryLabel); ?></span> <span class="badge <?php echo $this->escape($first->categoryBadge); ?>"><?php echo $this->escape($first->categoryLabel); ?></span>
</h3> </h3>
<div class="mokowaas-plugin-grid row g-3 mb-4"> <div class="mokosuite-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 col-md-6">
<div class="card mokowaas-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokowaas-plugin-disabled'; ?>" <div class="card mokosuite-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokosuite-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">
<div class="d-flex align-items-start justify-content-between mb-2"> <div class="d-flex align-items-start justify-content-between mb-2">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="<?php echo $this->escape($plugin->icon); ?> mokowaas-plugin-icon" aria-hidden="true"></span> <span class="<?php echo $this->escape($plugin->icon); ?> mokosuite-plugin-icon" aria-hidden="true"></span>
<h5 class="card-title mb-0"><?php echo $this->escape($plugin->name); ?></h5> <h5 class="card-title mb-0"><?php echo $this->escape($plugin->name); ?></h5>
</div> </div>
<?php if ($plugin->version): ?> <?php if ($plugin->version): ?>
@@ -213,27 +213,27 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<p class="card-text text-muted text-muted flex-grow-1"><?php echo $this->escape($plugin->description); ?></p> <p class="card-text text-muted text-muted flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<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_MOKOWAAS_PROTECTED'); ?></span> <span class="badge bg-dark"><?php echo Text::_('COM_MOKOSUITE_PROTECTED'); ?></span>
<?php elseif ($plugin->configure_only): ?> <?php elseif ($plugin->configure_only): ?>
<span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>"> <span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?> <?php echo $plugin->enabled ? Text::_('COM_MOKOSUITE_ENABLED') : Text::_('COM_MOKOSUITE_DISABLED'); ?>
</span> </span>
<?php else: ?> <?php else: ?>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokowaas-toggle" role="switch" <input type="checkbox" class="form-check-input mokosuite-toggle" role="switch"
id="toggle-<?php echo $plugin->extension_id; ?>" id="toggle-<?php echo $plugin->extension_id; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>" data-extension-id="<?php echo $plugin->extension_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.togglePlugin&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.togglePlugin&format=json'); ?>"
data-token="<?php echo $token; ?>" data-token="<?php echo $token; ?>"
<?php echo $plugin->enabled ? 'checked' : ''; ?>> <?php echo $plugin->enabled ? 'checked' : ''; ?>>
<label class="form-check-label" for="toggle-<?php echo $plugin->extension_id; ?>"> <label class="form-check-label" for="toggle-<?php echo $plugin->extension_id; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?> <?php echo $plugin->enabled ? Text::_('COM_MOKOSUITE_ENABLED') : Text::_('COM_MOKOSUITE_DISABLED'); ?>
</label> </label>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if ($plugin->type === 'plugin'): ?> <?php if ($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_MOKOWAAS_CONFIGURE'); ?> <span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOSUITE_CONFIGURE'); ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -254,7 +254,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<strong><span class="icon-shield-alt" aria-hidden="true"></span> WAF Activity (14 days)</strong> <strong><span class="icon-shield-alt" aria-hidden="true"></span> WAF Activity (14 days)</strong>
</div> </div>
<div class="card-body py-2"> <div class="card-body py-2">
<canvas id="mokowaas-chart-waf" height="140"></canvas> <canvas id="mokosuite-chart-waf" height="140"></canvas>
</div> </div>
</div> </div>
@@ -264,7 +264,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<strong><span class="icon-user" aria-hidden="true"></span> Login Activity (14 days)</strong> <strong><span class="icon-user" aria-hidden="true"></span> Login Activity (14 days)</strong>
</div> </div>
<div class="card-body py-2"> <div class="card-body py-2">
<canvas id="mokowaas-chart-logins" height="140"></canvas> <canvas id="mokosuite-chart-logins" height="140"></canvas>
</div> </div>
</div> </div>
@@ -409,7 +409,7 @@ document.addEventListener('DOMContentLoaded', function() {
}; };
// WAF chart // WAF chart
var wafCtx = document.getElementById('mokowaas-chart-waf'); var wafCtx = document.getElementById('mokosuite-chart-waf');
if (wafCtx) { if (wafCtx) {
new Chart(wafCtx, { new Chart(wafCtx, {
type: 'bar', type: 'bar',
@@ -428,7 +428,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Login chart // Login chart
var loginCtx = document.getElementById('mokowaas-chart-logins'); var loginCtx = document.getElementById('mokosuite-chart-logins');
if (loginCtx) { if (loginCtx) {
new Chart(loginCtx, { new Chart(loginCtx, {
type: 'line', type: 'line',
@@ -7,12 +7,12 @@ use Joomla\CMS\Session\Session;
$data = $this->tableData; $data = $this->tableData;
$tables = $data['tables'] ?? []; $tables = $data['tables'] ?? [];
$token = Session::getFormToken(); $token = Session::getFormToken();
$optimizeUrl = Route::_('index.php?option=com_mokowaas&task=display.optimizeDb&format=json'); $optimizeUrl = Route::_('index.php?option=com_mokosuite&task=display.optimizeDb&format=json');
$repairUrl = Route::_('index.php?option=com_mokowaas&task=display.repairDb&format=json'); $repairUrl = Route::_('index.php?option=com_mokosuite&task=display.repairDb&format=json');
$purgeUrl = Route::_('index.php?option=com_mokowaas&task=display.purgeSessions&format=json'); $purgeUrl = Route::_('index.php?option=com_mokosuite&task=display.purgeSessions&format=json');
?> ?>
<div id="mokowaas-database"> <div id="mokosuite-database">
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['count']; ?></span><small class="text-muted">Tables</small></div></div> <div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['count']; ?></span><small class="text-muted">Tables</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['total_size_mb']; ?> MB</span><small class="text-muted">Total Size</small></div></div> <div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['total_size_mb']; ?> MB</span><small class="text-muted">Total Size</small></div></div>
@@ -0,0 +1,71 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
$tab = $this->activeTab;
$totalRevenue = array_sum(array_map(fn($r) => (float) $r->revenue, $this->salesData));
$totalCollected = array_sum(array_map(fn($r) => (float) $r->collected, $this->salesData));
$wonDeals = $this->pipelineData['won'] ?? (object) ['cnt' => 0, 'total_value' => 0];
$lostDeals = $this->pipelineData['lost'] ?? (object) ['cnt' => 0, 'total_value' => 0];
$openDeals = $this->pipelineData['open'] ?? (object) ['cnt' => 0, 'total_value' => 0];
$closedTotal = ((int) ($wonDeals->cnt ?? 0)) + ((int) ($lostDeals->cnt ?? 0));
$winRate = $closedTotal > 0 ? round((int) ($wonDeals->cnt ?? 0) / $closedTotal * 100, 1) : 0;
?>
<div class="mokosuite-erp-reports">
<div class="card shadow-sm mb-3"><div class="card-body py-2">
<form method="get" class="d-flex gap-2 align-items-center flex-wrap">
<input type="hidden" name="option" value="com_mokosuite"><input type="hidden" name="view" value="erpreports"><input type="hidden" name="tab" value="<?php echo $this->escape($tab); ?>">
<label class="form-label mb-0 small">From:</label><input type="date" name="from" class="form-control form-control-sm" style="max-width:160px" value="<?php echo $this->escape($this->dateFrom); ?>">
<label class="form-label mb-0 small">To:</label><input type="date" name="to" class="form-control form-control-sm" style="max-width:160px" value="<?php echo $this->escape($this->dateTo); ?>">
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
</form>
</div></div>
<ul class="nav nav-tabs mb-3">
<li class="nav-item"><a class="nav-link <?php echo $tab === 'sales' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=sales&from=' . $this->dateFrom . '&to=' . $this->dateTo); ?>">Sales</a></li>
<li class="nav-item"><a class="nav-link <?php echo $tab === 'pipeline' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=pipeline&from=' . $this->dateFrom . '&to=' . $this->dateTo); ?>">Pipeline</a></li>
<li class="nav-item"><a class="nav-link <?php echo $tab === 'aging' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=aging'); ?>">Aging Receivables</a></li>
</ul>
<?php if ($tab === 'sales') : ?>
<div class="row g-3 mb-3">
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Revenue</div><div class="fs-3 fw-bold">$<?php echo number_format($totalRevenue, 0); ?></div></div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Collected</div><div class="fs-3 fw-bold text-success">$<?php echo number_format($totalCollected, 0); ?></div></div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Outstanding</div><div class="fs-3 fw-bold text-danger">$<?php echo number_format($totalRevenue - $totalCollected, 0); ?></div></div></div></div>
</div>
<div class="card shadow-sm mb-3"><div class="card-header"><h5 class="mb-0">Revenue by Period</h5></div><div class="card-body"><canvas id="erp-revenue-chart" height="300"></canvas></div></div>
<div class="row g-3">
<div class="col-md-6"><div class="card shadow-sm"><div class="card-header"><h5 class="mb-0">Top Customers</h5></div><div class="card-body p-0"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>Customer</th><th class="text-end">Revenue</th><th class="text-end">Invoices</th></tr></thead><tbody>
<?php foreach ($this->topCustomers as $c) : ?><tr><td><?php echo $this->escape($c->contact_name ?? '—'); ?></td><td class="text-end fw-bold">$<?php echo number_format((float) $c->total_revenue, 0); ?></td><td class="text-end"><?php echo (int) $c->invoice_count; ?></td></tr><?php endforeach; ?>
</tbody></table></div></div></div>
<div class="col-md-6"><div class="card shadow-sm"><div class="card-header"><h5 class="mb-0">Top Products</h5></div><div class="card-body p-0"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>Product</th><th class="text-end">Qty</th><th class="text-end">Revenue</th></tr></thead><tbody>
<?php foreach ($this->topProducts as $p) : ?><tr><td><?php echo $this->escape($p->product_name ?? $p->sku); ?></td><td class="text-end"><?php echo number_format((float) $p->qty_sold, 0); ?></td><td class="text-end fw-bold">$<?php echo number_format((float) $p->revenue, 0); ?></td></tr><?php endforeach; ?>
</tbody></table></div></div></div>
</div>
<?php elseif ($tab === 'pipeline') : ?>
<div class="row g-3 mb-3">
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Open</div><div class="fs-3 fw-bold"><?php echo (int) ($openDeals->cnt ?? 0); ?></div><div class="small">$<?php echo number_format((float) ($openDeals->total_value ?? 0), 0); ?></div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Won</div><div class="fs-3 fw-bold text-success"><?php echo (int) ($wonDeals->cnt ?? 0); ?></div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Lost</div><div class="fs-3 fw-bold text-danger"><?php echo (int) ($lostDeals->cnt ?? 0); ?></div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Win Rate</div><div class="fs-3 fw-bold"><?php echo $winRate; ?>%</div></div></div></div>
</div>
<?php elseif ($tab === 'aging') : ?>
<?php $buckets = ['current' => 0, '1_30' => 0, '31_60' => 0, '61_90' => 0, 'over_90' => 0];
foreach ($this->agingData as $r) { $d = (int) $r->days_overdue; if ($d <= 0) $buckets['current'] += (float) $r->balance; elseif ($d <= 30) $buckets['1_30'] += (float) $r->balance; elseif ($d <= 60) $buckets['31_60'] += (float) $r->balance; elseif ($d <= 90) $buckets['61_90'] += (float) $r->balance; else $buckets['over_90'] += (float) $r->balance; }
$totalAging = array_sum($buckets); ?>
<div class="row g-3 mb-3">
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Current</div><div class="fw-bold">$<?php echo number_format($buckets['current'], 0); ?></div></div></div></div>
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">1-30 Days</div><div class="fw-bold text-warning">$<?php echo number_format($buckets['1_30'], 0); ?></div></div></div></div>
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">31-60</div><div class="fw-bold text-warning">$<?php echo number_format($buckets['31_60'], 0); ?></div></div></div></div>
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">61-90</div><div class="fw-bold text-danger">$<?php echo number_format($buckets['61_90'], 0); ?></div></div></div></div>
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">90+</div><div class="fw-bold text-danger">$<?php echo number_format($buckets['over_90'], 0); ?></div></div></div></div>
</div>
<div class="card shadow-sm"><div class="card-body p-0"><table class="table table-sm table-hover mb-0"><thead class="table-light"><tr><th>Ref</th><th>Contact</th><th class="text-end">Total</th><th class="text-end">Paid</th><th class="text-end">Balance</th><th>Due</th><th>Days</th></tr></thead><tbody>
<?php foreach ($this->agingData as $r) : ?>
<tr class="<?php echo (int) $r->days_overdue > 60 ? 'table-danger' : ((int) $r->days_overdue > 30 ? 'table-warning' : ''); ?>">
<td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpinvoice&id=' . (int) $r->id); ?>" class="font-monospace"><?php echo $this->escape($r->ref); ?></a></td>
<td><?php echo $this->escape($r->contact_name ?? '—'); ?></td><td class="text-end">$<?php echo number_format((float) $r->total, 2); ?></td>
<td class="text-end">$<?php echo number_format((float) $r->amount_paid, 2); ?></td><td class="text-end fw-bold text-danger">$<?php echo number_format((float) $r->balance, 2); ?></td>
<td class="small"><?php echo $this->escape($r->due_date); ?></td><td class="fw-bold"><?php echo (int) $r->days_overdue; ?></td>
</tr><?php endforeach; ?>
</tbody><tfoot class="table-light"><tr><td colspan="4" class="text-end fw-bold">Total</td><td class="text-end fw-bold text-danger">$<?php echo number_format($totalAging, 2); ?></td><td colspan="2"></td></tr></tfoot></table></div></div>
<?php endif; ?>
</div>
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
@@ -12,7 +12,7 @@ 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;
/** @var \Moko\Component\MokoWaaS\Administrator\View\Extensions\HtmlView $this */ /** @var \Moko\Component\MokoSuite\Administrator\View\Extensions\HtmlView $this */
$packages = $this->packages; $packages = $this->packages;
$token = Session::getFormToken(); $token = Session::getFormToken();
@@ -31,10 +31,10 @@ $statusBadge = [
]; ];
?> ?>
<div id="mokowaas-extensions"> <div id="mokosuite-extensions">
<div class="alert alert-info"> <div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span> <span class="icon-info-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_INFO'); ?> <?php echo Text::_('COM_MOKOSUITE_EXTENSIONS_INFO'); ?>
</div> </div>
<?php foreach ($grouped as $category => $pkgs): ?> <?php foreach ($grouped as $category => $pkgs): ?>
@@ -44,7 +44,7 @@ $statusBadge = [
<?php <?php
$badge = $statusBadge[$pkg->status] ?? $statusBadge['not_installed']; $badge = $statusBadge[$pkg->status] ?? $statusBadge['not_installed'];
?> ?>
<div class="col-12 col-md-6 col-xl-4"> <div class="col-12 <?php echo \count($pkgs) === 1 ? '' : (\count($pkgs) === 2 ? 'col-md-6' : 'col-md-6 col-xl-4'); ?>">
<div class="card h-100"> <div class="card h-100">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<div class="d-flex align-items-start justify-content-between mb-2"> <div class="d-flex align-items-start justify-content-between mb-2">
@@ -60,6 +60,14 @@ $statusBadge = [
<p class="card-text text-muted flex-grow-1"><?php echo htmlspecialchars($pkg->description); ?></p> <p class="card-text text-muted flex-grow-1"><?php echo htmlspecialchars($pkg->description); ?></p>
<?php if (!empty($pkg->needs_dlid) && !$pkg->has_dlid && $pkg->status !== 'not_installed'): ?>
<div class="alert alert-danger py-1 px-2 mb-2" style="font-size:0.8rem;">
<span class="icon-exclamation-triangle" aria-hidden="true"></span>
Download key missing updates will fail.
<a href="index.php?option=com_installer&view=updatesites" class="alert-link">Configure</a>
</div>
<?php endif; ?>
<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">
<div class="small text-muted"> <div class="small text-muted">
<?php if ($pkg->local_version): ?> <?php if ($pkg->local_version): ?>
@@ -78,20 +86,24 @@ $statusBadge = [
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($pkg->download_url && $pkg->status === 'update_available'): ?> <?php if ($pkg->download_url && $pkg->status === 'update_available'): ?>
<button type="button" class="btn btn-sm btn-warning mokowaas-install-btn" <button type="button" class="btn btn-sm btn-warning mokosuite-install-btn"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>" data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
data-token="<?php echo $token; ?>" data-token="<?php echo $token; ?>"
data-label="<?php echo htmlspecialchars($pkg->label); ?>"> data-label="<?php echo htmlspecialchars($pkg->label); ?>"
data-needs-dlid="<?php echo $pkg->needs_dlid ? '1' : '0'; ?>"
data-element="<?php echo htmlspecialchars($pkg->element); ?>">
<span class="icon-refresh" aria-hidden="true"></span> <span class="icon-refresh" aria-hidden="true"></span>
Update to <?php echo htmlspecialchars($pkg->remote_version); ?> Update to <?php echo htmlspecialchars($pkg->remote_version); ?>
</button> </button>
<?php elseif ($pkg->download_url && $pkg->status === 'not_installed'): ?> <?php elseif ($pkg->download_url && $pkg->status === 'not_installed'): ?>
<button type="button" class="btn btn-sm btn-primary mokowaas-install-btn" <button type="button" class="btn btn-sm btn-primary mokosuite-install-btn"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>" data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
data-token="<?php echo $token; ?>" data-token="<?php echo $token; ?>"
data-label="<?php echo htmlspecialchars($pkg->label); ?>"> data-label="<?php echo htmlspecialchars($pkg->label); ?>"
data-needs-dlid="<?php echo $pkg->needs_dlid ? '1' : '0'; ?>"
data-element="<?php echo htmlspecialchars($pkg->element); ?>">
<span class="icon-download" aria-hidden="true"></span> <span class="icon-download" aria-hidden="true"></span>
Install Install
</button> </button>
@@ -142,7 +154,7 @@ $statusBadge = [
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokowaas-install-btn').forEach(function(btn) { document.querySelectorAll('.mokosuite-install-btn').forEach(function(btn) {
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var el = this; var el = this;
var url = el.dataset.url; var url = el.dataset.url;
@@ -150,15 +162,37 @@ document.addEventListener('DOMContentLoaded', function() {
var token = el.dataset.token; var token = el.dataset.token;
var label = el.dataset.label; var label = el.dataset.label;
var needsDlid = el.dataset.needsDlid === '1';
var dlid = '';
if (needsDlid) {
dlid = prompt('Enter download key for ' + label + ':', '');
if (dlid === null) return;
if (!dlid.trim()) {
Joomla.renderMessages({error: ['Download key is required for ' + label]});
return;
}
}
if (!confirm('Install ' + label + '?')) return; if (!confirm('Install ' + label + '?')) return;
el.disabled = true; el.disabled = true;
var origHtml = el.textContent; var origHtml = el.textContent;
el.textContent = ' Installing...'; el.textContent = ' Installing...';
// Append dlid to download URL if provided
var finalUrl = downloadUrl;
if (dlid) {
finalUrl += (downloadUrl.indexOf('?') !== -1 ? '&' : '?') + 'dlid=' + encodeURIComponent(dlid.trim());
}
var fd = new FormData(); var fd = new FormData();
fd.append('download_url', downloadUrl); fd.append('download_url', finalUrl);
fd.append(token, '1'); fd.append(token, '1');
if (dlid) {
fd.append('dlid', dlid.trim());
fd.append('element', el.dataset.element || '');
}
fetch(url, { fetch(url, {
method: 'POST', method: 'POST',
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
@@ -17,8 +17,8 @@ $preview = $this->preview;
$nginx = $this->nginxPreview; $nginx = $this->nginxPreview;
$current = $this->currentHtaccess; $current = $this->currentHtaccess;
$token = Session::getFormToken(); $token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveHtaccess&format=json'); $saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveHtaccess&format=json');
$genUrl = Route::_('index.php?option=com_mokowaas&task=display.generateHtaccess&format=json'); $genUrl = Route::_('index.php?option=com_mokosuite&task=display.generateHtaccess&format=json');
// Helper for toggle switch // Helper for toggle switch
$sw = function($name, $label, $desc = '') use ($opts) { $sw = function($name, $label, $desc = '') use ($opts) {
@@ -33,7 +33,7 @@ $sw = function($name, $label, $desc = '') use ($opts) {
}; };
?> ?>
<div id="mokowaas-htaccess"> <div id="mokosuite-htaccess">
<ul class="nav nav-tabs mb-3" role="tablist"> <ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-htaccess" role="tab">.htaccess</a></li> <li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-htaccess" role="tab">.htaccess</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-nginx" role="tab">NginX</a></li> <li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-nginx" role="tab">NginX</a></li>
@@ -263,7 +263,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Save to disk // Save to disk
saveBtn.addEventListener('click', function() { saveBtn.addEventListener('click', function() {
if (!confirm('This will overwrite your current .htaccess file. A backup will be created at .htaccess.mokowaas.bak. Continue?')) return; if (!confirm('This will overwrite your current .htaccess file. A backup will be created at .htaccess.mokosuite.bak. Continue?')) return;
var btn = this; var btn = this;
btn.disabled = true; btn.disabled = true;
@@ -300,7 +300,7 @@ document.addEventListener('DOMContentLoaded', function() {
downloadText(preview.value, '.htaccess'); downloadText(preview.value, '.htaccess');
}); });
document.getElementById('nginx-download').addEventListener('click', function() { document.getElementById('nginx-download').addEventListener('click', function() {
downloadText(document.getElementById('nginx-preview').value, 'mokowaas-nginx.conf'); downloadText(document.getElementById('nginx-preview').value, 'mokosuite-nginx.conf');
}); });
}); });
</script> </script>
@@ -24,7 +24,7 @@ $typeBadge = [
]; ];
?> ?>
<div id="mokowaas-privacy"> <div id="mokosuite-privacy">
<!-- Summary cards --> <!-- Summary cards -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
@@ -100,7 +100,7 @@ $typeBadge = [
</div> </div>
<div class="col-12 col-md-2 d-flex align-items-end"> <div class="col-12 col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100" id="btnCreateRequest" <button type="submit" class="btn btn-primary w-100" id="btnCreateRequest"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-check"></span> Submit <span class="icon-check"></span> Submit
</button> </button>
@@ -117,7 +117,7 @@ $typeBadge = [
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-user-shield"></span> Data Subject Requests</strong> <strong><span class="icon-user-shield"></span> Data Subject Requests</strong>
<form method="get" class="d-inline"> <form method="get" class="d-inline">
<input type="hidden" name="option" value="com_mokowaas"> <input type="hidden" name="option" value="com_mokosuite">
<input type="hidden" name="view" value="privacy"> <input type="hidden" name="view" value="privacy">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()"> <select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All</option> <option value="">All</option>
@@ -146,15 +146,15 @@ $typeBadge = [
<?php if ($r->status === 'pending'): ?> <?php if ($r->status === 'pending'): ?>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button type="button" class="btn btn-success btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="approve" <button type="button" class="btn btn-success btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="approve"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Approve</button> data-token="<?php echo $token; ?>">Approve</button>
<button type="button" class="btn btn-outline-danger btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="deny" <button type="button" class="btn btn-outline-danger btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="deny"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Deny</button> data-token="<?php echo $token; ?>">Deny</button>
</div> </div>
<?php elseif ($r->status === 'completed' && $r->type === 'export'): ?> <?php elseif ($r->status === 'completed' && $r->type === 'export'): ?>
<button type="button" class="btn btn-sm btn-outline-primary btn-export-download" data-user="<?php echo $r->user_id; ?>" <button type="button" class="btn btn-sm btn-outline-primary btn-export-download" data-user="<?php echo $r->user_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.exportUserData&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.exportUserData&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-download"></span> Download <span class="icon-download"></span> Download
</button> </button>
@@ -0,0 +1,364 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
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_mokosuite&task=display.downloadAttachment');
$uploadUrl = Route::_('index.php?option=com_mokosuite&task=display.uploadAttachment&format=json');
$deleteAttUrl = Route::_('index.php?option=com_mokosuite&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="mokosuite-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, 'M d, Y H:i'); ?></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\MokoSuite\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, 'M d, Y H:i'); ?></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\MokoSuite\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_mokosuite&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_mokosuite&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, 'M d, Y H:i'); ?></td></tr>
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, 'M d, Y H:i'); ?></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, 'M d H:i'); ?>
<?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, 'M d H:i'); ?>
<?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_mokosuite&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_mokosuite&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>
@@ -9,35 +9,20 @@ use Joomla\CMS\Session\Session;
$tickets = $this->tickets; $tickets = $this->tickets;
$categories = $this->categories; $categories = $this->categories;
$statuses = $this->statuses;
$priorities = $this->priorities;
$counts = $this->statusCounts; $counts = $this->statusCounts;
$overdue = $this->overdue; $overdue = $this->overdue;
$atsAvailable = $this->atsAvailable; $atsAvailable = $this->atsAvailable;
$token = Session::getFormToken(); $token = Session::getFormToken();
$statusBadge = [
'open' => 'bg-primary',
'in_progress' => 'bg-info',
'waiting' => 'bg-warning text-dark',
'resolved' => 'bg-success',
'closed' => 'bg-secondary',
];
$priorityBadge = [
'low' => 'bg-secondary',
'normal' => 'bg-primary',
'high' => 'bg-warning text-dark',
'urgent' => 'bg-danger',
];
?> ?>
<div id="mokowaas-tickets"> <div id="mokosuite-tickets">
<!-- Status summary cards --> <!-- Status summary cards -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->open; ?></span><small class="text-muted">Open</small></div></div> <?php foreach ($counts as $sc): ?>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->in_progress; ?></span><small class="text-muted">In Progress</small></div></div> <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>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->waiting; ?></span><small class="text-muted">Waiting</small></div></div> <?php endforeach; ?>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->resolved; ?></span><small class="text-muted">Resolved</small></div></div>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->closed; ?></span><small class="text-muted">Closed</small></div></div>
<?php if (\count($overdue) > 0): ?> <?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> <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; ?> <?php endif; ?>
@@ -51,7 +36,7 @@ $priorityBadge = [
</button> </button>
<?php if ($atsAvailable): ?> <?php if ($atsAvailable): ?>
<button type="button" class="btn btn-outline-info" id="btn-import-ats" <button type="button" class="btn btn-outline-info" id="btn-import-ats"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAts&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>" data-token="<?php echo $token; ?>"
data-tickets="<?php echo $atsAvailable->tickets; ?>" data-tickets="<?php echo $atsAvailable->tickets; ?>"
data-posts="<?php echo $atsAvailable->posts; ?>"> data-posts="<?php echo $atsAvailable->posts; ?>">
@@ -60,18 +45,18 @@ $priorityBadge = [
<?php endif; ?> <?php endif; ?>
</div> </div>
<form method="get" class="d-flex gap-2"> <form method="get" class="d-flex gap-2">
<input type="hidden" name="option" value="com_mokowaas"> <input type="hidden" name="option" value="com_mokosuite">
<input type="hidden" name="view" value="tickets"> <input type="hidden" name="view" value="tickets">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()"> <select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Statuses</option> <option value="">All Statuses</option>
<?php foreach (['open','in_progress','waiting','resolved','closed'] as $s): ?> <?php foreach ($statuses as $s): ?>
<option value="<?php echo $s; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $s ? 'selected' : ''; ?>><?php echo ucwords(str_replace('_', ' ', $s)); ?></option> <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; ?> <?php endforeach; ?>
</select> </select>
<select name="filter_priority" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()"> <select name="filter_priority" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Priorities</option> <option value="">All Priorities</option>
<?php foreach (['low','normal','high','urgent'] as $p): ?> <?php foreach ($priorities as $p): ?>
<option value="<?php echo $p; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_priority') === $p ? 'selected' : ''; ?>><?php echo ucfirst($p); ?></option> <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; ?> <?php endforeach; ?>
</select> </select>
</form> </form>
@@ -88,6 +73,7 @@ $priorityBadge = [
<th>Status</th> <th>Status</th>
<th>Priority</th> <th>Priority</th>
<th>Category</th> <th>Category</th>
<th>Contact</th>
<th>Created By</th> <th>Created By</th>
<th>Assigned To</th> <th>Assigned To</th>
<th>Created</th> <th>Created</th>
@@ -96,24 +82,36 @@ $priorityBadge = [
</thead> </thead>
<tbody> <tbody>
<?php if (empty($tickets)): ?> <?php if (empty($tickets)): ?>
<tr><td colspan="9" class="text-center text-muted py-4">No tickets found.</td></tr> <tr><td colspan="10" class="text-center text-muted py-4">No tickets found.</td></tr>
<?php else: ?> <?php else: ?>
<?php foreach ($tickets as $t): ?> <?php foreach ($tickets as $t): ?>
<?php <?php
$slaClass = ''; $slaClass = '';
$now = time(); $now = time();
if ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now) $slaClass = 'table-danger'; 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 && !\in_array($t->status, ['resolved','closed'])) $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'; elseif ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now + 3600) $slaClass = 'table-warning';
?> ?>
<tr class="<?php echo $slaClass; ?>"> <tr class="<?php echo $slaClass; ?>">
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td> <td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo $this->escape(mb_substr($t->subject, 0, 60)); ?></a></td> <td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=ticket&id=' . $t->id); ?>"><?php echo $this->escape(mb_substr($t->subject, 0, 60)); ?></a></td>
<td><span class="badge <?php echo $statusBadge[$t->status] ?? 'bg-secondary'; ?>"><?php echo ucwords(str_replace('_', ' ', $t->status)); ?></span></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 $priorityBadge[$t->priority] ?? 'bg-secondary'; ?>"><?php echo ucfirst($t->priority); ?></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 $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 echo $this->escape($t->created_by_name ?? ''); ?></td>
<td><?php echo $t->assigned_to_name ? $this->escape($t->assigned_to_name) : '<em>Unassigned</em>'; ?></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, 'M d H:i'); ?></td> <td class="small"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></td>
<td class="small"> <td class="small">
<?php if ($t->sla_response_due && !$t->sla_responded): ?> <?php if ($t->sla_response_due && !$t->sla_responded): ?>
@@ -151,13 +149,13 @@ $priorityBadge = [
</div> </div>
<!-- Ticket form step (hidden initially) --> <!-- Ticket form step (hidden initially) -->
<form id="modal-ticket-form" class="d-none" method="post" action="<?php echo Route::_('index.php?option=com_mokowaas&task=display.createTicket&format=json'); ?>"> <form id="modal-ticket-form" class="d-none" method="post" action="<?php echo Route::_('index.php?option=com_mokosuite&task=display.createTicket&format=json'); ?>">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Subject</label> <label class="form-label">Subject</label>
<input type="text" name="subject" id="modal-subject" class="form-control" required> <input type="text" name="subject" id="modal-subject" class="form-control" required>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label">Category</label> <label class="form-label">Category</label>
<select name="category_id" class="form-select"> <select name="category_id" class="form-select">
<option value=""> Select </option> <option value=""> Select </option>
@@ -166,13 +164,21 @@ $priorityBadge = [
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label">Priority</label> <label class="form-label">Priority</label>
<select name="priority" class="form-select"> <select name="priority_id" class="form-select">
<option value="normal">Normal</option> <?php foreach ($priorities as $p): ?>
<option value="low">Low</option> <option value="<?php echo $p->id; ?>" <?php echo $p->is_default ? 'selected' : ''; ?>><?php echo $this->escape($p->title); ?></option>
<option value="high">High</option> <?php endforeach; ?>
<option value="urgent">Urgent</option> </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> </select>
</div> </div>
</div> </div>
@@ -204,7 +210,7 @@ var modalSubject = document.getElementById('modal-subject');
function modalDoSearch() { function modalDoSearch() {
var q = modalSearch.value.trim(); var q = modalSearch.value.trim();
if (q.length < 3) return; if (q.length < 3) return;
fetch('<?php echo Route::_('index.php?option=com_mokowaas&task=display.searchKb&format=json'); ?>&q=' + encodeURIComponent(q), { fetch('<?php echo Route::_('index.php?option=com_mokosuite&task=display.searchKb&format=json'); ?>&q=' + encodeURIComponent(q), {
headers: {'X-Requested-With': 'XMLHttpRequest'} headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d) { }).then(function(r){return r.json()}).then(function(d) {
modalResults.textContent = ''; modalResults.textContent = '';
@@ -254,7 +260,7 @@ if (modalForm) {
fetch(form.action, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}}) fetch(form.action, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()}) .then(function(r){return r.json()})
.then(function(d){ .then(function(d){
if (d.success) { location.href = 'index.php?option=com_mokowaas&view=ticket&id=' + d.id; } if (d.success) { location.href = 'index.php?option=com_mokosuite&view=ticket&id=' + d.id; }
else { Joomla.renderMessages({error:[d.message]}); } else { Joomla.renderMessages({error:[d.message]}); }
}); });
}); });
@@ -25,7 +25,7 @@ $ruleBadge = [
]; ];
?> ?>
<div id="mokowaas-waflog"> <div id="mokosuite-waflog">
<!-- Rule distribution cards --> <!-- Rule distribution cards -->
<div class="d-flex flex-wrap gap-2 mb-4"> <div class="d-flex flex-wrap gap-2 mb-4">
<?php foreach ($ruleCounts as $rc): ?> <?php foreach ($ruleCounts as $rc): ?>
@@ -46,7 +46,7 @@ $ruleBadge = [
<!-- Filters --> <!-- Filters -->
<form method="get" class="card mb-3"> <form method="get" class="card mb-3">
<div class="card-body"> <div class="card-body">
<input type="hidden" name="option" value="com_mokowaas"> <input type="hidden" name="option" value="com_mokosuite">
<input type="hidden" name="view" value="waflog"> <input type="hidden" name="view" value="waflog">
<div class="row g-2"> <div class="row g-2">
<div class="col-md-2"> <div class="col-md-2">
@@ -71,7 +71,7 @@ $ruleBadge = [
</div> </div>
<div class="col-md-2 d-flex gap-1"> <div class="col-md-2 d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button> <button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a> <a href="<?php echo Route::_('index.php?option=com_mokosuite&view=waflog'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div> </div>
</div> </div>
</div> </div>
@@ -82,7 +82,7 @@ $ruleBadge = [
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<strong><?php echo number_format($total); ?> blocked requests</strong> <strong><?php echo number_format($total); ?> blocked requests</strong>
<button type="button" class="btn btn-sm btn-outline-danger" id="btn-purge" <button type="button" class="btn btn-sm btn-outline-danger" id="btn-purge"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.purgeWafLog&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.purgeWafLog&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-trash"></span> Purge Old Logs <span class="icon-trash"></span> Purge Old Logs
</button> </button>
@@ -106,7 +106,7 @@ $ruleBadge = [
<td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->user_agent, 0, 40)); ?></td> <td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->user_agent, 0, 40)); ?></td>
<td> <td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($log->ip); ?>" <button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($log->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban this IP"> data-token="<?php echo $token; ?>" title="Ban this IP">
<span class="icon-ban"></span> <span class="icon-ban"></span>
</button> </button>
@@ -124,10 +124,10 @@ $ruleBadge = [
<nav> <nav>
<ul class="pagination pagination-sm mb-0"> <ul class="pagination pagination-sm mb-0">
<?php if ($page > 1): ?> <?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page - 1)); ?>">Prev</a></li> <li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuite&view=waflog&page=' . ($page - 1)); ?>">Prev</a></li>
<?php endif; ?> <?php endif; ?>
<?php if ($page < $totalPages): ?> <?php if ($page < $totalPages): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page + 1)); ?>">Next</a></li> <li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuite&view=waflog&page=' . ($page + 1)); ?>">Next</a></li>
<?php endif; ?> <?php endif; ?>
</ul> </ul>
</nav> </nav>
@@ -151,7 +151,7 @@ $ruleBadge = [
<td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, 'M d'); ?></td> <td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, 'M d'); ?></td>
<td> <td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>" <button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban"> data-token="<?php echo $token; ?>" title="Ban">
<span class="icon-ban"></span> <span class="icon-ban"></span>
</button> </button>
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Cache management API controller. * Cache management API controller.
* *
* POST /api/index.php/v1/mokowaas/cache * POST /api/index.php/v1/mokosuite/cache
* *
* @since 1.0.0 * @since 1.0.0
*/ */
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -20,7 +20,7 @@ use Joomla\Registry\Registry;
/** /**
* Dashboard summary API controller. * Dashboard summary API controller.
* *
* GET /api/index.php/v1/mokowaas/dashboard * GET /api/index.php/v1/mokosuite/dashboard
* *
* Returns a combined payload of site info and feature plugin states, * Returns a combined payload of site info and feature plugin states,
* suitable for remote dashboards and monitoring. * suitable for remote dashboards and monitoring.
@@ -53,7 +53,7 @@ class DashboardController extends BaseController
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('manifest_cache')) ->select($db->quoteName('manifest_cache'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('package')); ->where($db->quoteName('type') . ' = ' . $db->quote('package'));
$db->setQuery($query); $db->setQuery($query);
$pkgCache = json_decode($db->loadResult() ?? '{}'); $pkgCache = json_decode($db->loadResult() ?? '{}');
@@ -71,8 +71,8 @@ class DashboardController extends BaseController
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite')
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\\_%') . ')') . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite\\_%') . ')')
->order($db->quoteName('element') . ' ASC'); ->order($db->quoteName('element') . ' ASC');
$db->setQuery($query); $db->setQuery($query);
$pluginRows = $db->loadObjectList() ?: []; $pluginRows = $db->loadObjectList() ?: [];
@@ -118,7 +118,7 @@ class DashboardController extends BaseController
'site' => [ 'site' => [
'name' => $config->get('sitename', ''), 'name' => $config->get('sitename', ''),
'url' => rtrim(Uri::root(), '/'), 'url' => rtrim(Uri::root(), '/'),
'mokowaas_version' => $pkgCache->version ?? '', 'mokosuite_version' => $pkgCache->version ?? '',
'joomla_version' => (new Version())->getShortVersion(), 'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'db_type' => $db->getServerType(), 'db_type' => $db->getServerType(),
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Extensions list API controller. * Extensions list API controller.
* *
* GET /api/index.php/v1/mokowaas/extensions * GET /api/index.php/v1/mokosuite/extensions
* *
* Returns all installed extensions with type, element, folder, version, * Returns all installed extensions with type, element, folder, version,
* enabled/protected/locked status, and update server info. * enabled/protected/locked status, and update server info.
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,9 +19,9 @@ use Joomla\Registry\Registry;
/** /**
* Health check API controller. * Health check API controller.
* *
* GET /api/index.php/v1/mokowaas/health * GET /api/index.php/v1/mokosuite/health
* *
* Returns full health diagnostics from the MokoWaaS system plugin. * Returns full health diagnostics from the MokoSuite system plugin.
* Requires a Joomla API token with core.manage permissions. * Requires a Joomla API token with core.manage permissions.
* *
* @since 1.0.0 * @since 1.0.0
@@ -46,11 +46,11 @@ class HealthController extends BaseController
return; return;
} }
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
if (!$plugin) if (!$plugin)
{ {
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']); $this->sendJson(503, ['error' => 'MokoSuite system plugin not enabled']);
return; return;
} }
@@ -74,7 +74,7 @@ class HealthController extends BaseController
'caching' => (bool) $config->get('caching', 0), 'caching' => (bool) $config->get('caching', 0),
], ],
'plugin' => [ 'plugin' => [
'brand' => $params->get('brand_name', 'MokoWaaS'), 'brand' => $params->get('brand_name', 'MokoSuite'),
'company' => $params->get('company_name', 'Moko Consulting'), 'company' => $params->get('company_name', 'Moko Consulting'),
], ],
]; ];
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -17,7 +17,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Extension install-from-URL API controller. * Extension install-from-URL API controller.
* *
* POST /api/index.php/v1/mokowaas/install * POST /api/index.php/v1/mokosuite/install
* Body: {"url": "https://example.com/path/to/extension.zip"} * Body: {"url": "https://example.com/path/to/extension.zip"}
* *
* Downloads a ZIP from the given URL and installs it via Joomla's Installer. * Downloads a ZIP from the given URL and installs it via Joomla's Installer.
@@ -115,7 +115,7 @@ class InstallController extends BaseController
{ {
$config = Factory::getConfig(); $config = Factory::getConfig();
$tmpPath = $config->get('tmp_path', JPATH_ROOT . '/tmp'); $tmpPath = $config->get('tmp_path', JPATH_ROOT . '/tmp');
$zipFile = $tmpPath . '/mokowaas_install_' . bin2hex(random_bytes(8)) . '.zip'; $zipFile = $tmpPath . '/mokosuite_install_' . bin2hex(random_bytes(8)) . '.zip';
// Download // Download
$this->downloadFile($url, $zipFile); $this->downloadFile($url, $zipFile);
@@ -123,7 +123,7 @@ class InstallController extends BaseController
try try
{ {
// Extract // Extract
$extractDir = $tmpPath . '/mokowaas_extract_' . bin2hex(random_bytes(8)); $extractDir = $tmpPath . '/mokosuite_extract_' . bin2hex(random_bytes(8));
if (!mkdir($extractDir, 0755, true)) if (!mkdir($extractDir, 0755, true))
{ {
@@ -207,7 +207,7 @@ class InstallController extends BaseController
CURLOPT_TIMEOUT => 120, CURLOPT_TIMEOUT => 120,
CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_FAILONERROR => true, CURLOPT_FAILONERROR => true,
CURLOPT_USERAGENT => 'MokoWaaS-Installer/1.0', CURLOPT_USERAGENT => 'MokoSuite-Installer/1.0',
]); ]);
$success = curl_exec($ch); $success = curl_exec($ch);
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,15 +16,15 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Feature plugins API controller. * Feature plugins API controller.
* *
* GET /api/index.php/v1/mokowaas/plugins list MokoWaaS plugins + status * GET /api/index.php/v1/mokosuite/plugins list MokoSuite plugins + status
* POST /api/index.php/v1/mokowaas/plugins/toggle enable/disable a feature plugin * POST /api/index.php/v1/mokosuite/plugins/toggle enable/disable a feature plugin
* *
* @since 02.32.00 * @since 02.32.00
*/ */
class PluginsController extends BaseController class PluginsController extends BaseController
{ {
/** /**
* List all MokoWaaS feature plugins with their enabled state. * List all MokoSuite feature plugins with their enabled state.
* *
* @return void * @return void
*/ */
@@ -57,14 +57,14 @@ class PluginsController extends BaseController
'(' . '(' .
'(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system')
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite')
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\\_%') . '))' . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite\\_%') . '))'
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices')
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ')' . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite') . ')'
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas%') . ')' . ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite%') . ')'
. ')', . ')',
]) ])
->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC'); ->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC');
@@ -98,7 +98,7 @@ class PluginsController extends BaseController
} }
/** /**
* Toggle a MokoWaaS feature plugin on or off. * Toggle a MokoSuite feature plugin on or off.
* *
* Expects JSON body: {"extension_id": 123, "enabled": true} * Expects JSON body: {"extension_id": 123, "enabled": true}
* *
@@ -130,7 +130,7 @@ class PluginsController extends BaseController
$db = Factory::getDbo(); $db = Factory::getDbo();
// Verify the extension exists and is a MokoWaaS plugin // Verify the extension exists and is a MokoSuite plugin
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select([$db->quoteName('element'), $db->quoteName('protected')]) ->select([$db->quoteName('element'), $db->quoteName('protected')])
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
@@ -147,7 +147,7 @@ class PluginsController extends BaseController
} }
// Don't allow disabling protected/core plugins // Don't allow disabling protected/core plugins
if (!$enabled && ((int) $ext->protected || $ext->element === 'mokowaas')) if (!$enabled && ((int) $ext->protected || $ext->element === 'mokosuite'))
{ {
$this->sendJson(409, ['error' => 'This plugin is protected and cannot be disabled']); $this->sendJson(409, ['error' => 'This plugin is protected and cannot be disabled']);
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Provision reset API controller. * Provision reset API controller.
* *
* POST /api/index.php/v1/mokowaas/provision-reset * POST /api/index.php/v1/mokosuite/provision-reset
* *
* Resets a site for new client provisioning: clears hits, versions, * Resets a site for new client provisioning: clears hits, versions,
* download keys, and flags the site for fresh client info collection. * download keys, and flags the site for fresh client info collection.
@@ -36,7 +36,7 @@ class ProvisionController extends BaseController
$app = Factory::getApplication(); $app = Factory::getApplication();
$user = $app->getIdentity(); $user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokowaas')) if (!$user->authorise('core.manage', 'com_mokosuite'))
{ {
$this->sendJson(403, ['error' => 'Not authorized']); $this->sendJson(403, ['error' => 'Not authorized']);
@@ -91,7 +91,7 @@ class ProvisionController extends BaseController
{ {
$newToken = bin2hex(random_bytes(32)); $newToken = bin2hex(random_bytes(32));
$plugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas'); $plugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite');
if ($plugin) if ($plugin)
{ {
@@ -102,7 +102,7 @@ class ProvisionController extends BaseController
$db->getQuery(true) $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($pluginParams->toString())) ->set($db->quoteName('params') . ' = ' . $db->quote($pluginParams->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system')) ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute(); )->execute();
@@ -156,7 +156,7 @@ class ProvisionController extends BaseController
try try
{ {
// Write a flag file that the core plugin checks on next admin load // Write a flag file that the core plugin checks on next admin load
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag'; $flagFile = JPATH_ADMINISTRATOR . '/cache/mokosuite_setup_required.flag';
file_put_contents($flagFile, json_encode([ file_put_contents($flagFile, json_encode([
'created' => gmdate('Y-m-d\TH:i:s\Z'), 'created' => gmdate('Y-m-d\TH:i:s\Z'),
'reason' => 'provision-reset', 'reason' => 'provision-reset',
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,8 +19,8 @@ use Joomla\Registry\Registry;
/** /**
* Remote login API controller. * Remote login API controller.
* *
* POST /api/index.php/v1/mokowaas/remote-login * POST /api/index.php/v1/mokosuite/remote-login
* Body: {"token": "health_api_token", "user": "requesting_username", "origin": "MokoWaaSBase"} * Body: {"token": "health_api_token", "user": "requesting_username", "origin": "MokoSuiteHQ"}
* *
* Validates the health API token, generates a one-time login token * Validates the health API token, generates a one-time login token
* for the master user, and returns a URL that auto-authenticates. * for the master user, and returns a URL that auto-authenticates.
@@ -55,11 +55,11 @@ class RemoteLoginController extends BaseController
} }
// Validate against the core plugin's health_api_token // Validate against the core plugin's health_api_token
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
if (!$plugin) if (!$plugin)
{ {
$this->sendJson(503, ['error' => 'MokoWaaS core plugin not found']); $this->sendJson(503, ['error' => 'MokoSuite core plugin not found']);
return; return;
} }
@@ -110,7 +110,7 @@ class RemoteLoginController extends BaseController
$expires = time() + self::OTL_TTL; $expires = time() + self::OTL_TTL;
// Store in a temp file (avoids DB schema changes) // Store in a temp file (avoids DB schema changes)
$otlFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_otl_' . md5($otlToken) . '.json'; $otlFile = JPATH_ADMINISTRATOR . '/cache/mokosuite_otl_' . md5($otlToken) . '.json';
file_put_contents($otlFile, json_encode([ file_put_contents($otlFile, json_encode([
'token' => $otlToken, 'token' => $otlToken,
'user_id' => (int) $user->id, 'user_id' => (int) $user->id,
@@ -120,7 +120,7 @@ class RemoteLoginController extends BaseController
])); ]));
// Build login URL // Build login URL
$loginUrl = rtrim(Uri::root(), '/') . '/administrator/index.php?mokowaas_otl=' . $otlToken; $loginUrl = rtrim(Uri::root(), '/') . '/administrator/index.php?mokosuite_otl=' . $otlToken;
$this->sendJson(200, [ $this->sendJson(200, [
'status' => 'ok', 'status' => 'ok',
@@ -139,16 +139,16 @@ class RemoteLoginController extends BaseController
*/ */
private function getMasterUsernames(Registry $params): array private function getMasterUsernames(Registry $params): array
{ {
// Use MokoWaaSHelper if available // Use MokoSuiteHelper if available
$helperFile = JPATH_PLUGINS . '/system/mokowaas/Helper/MokoWaaSHelper.php'; $helperFile = JPATH_PLUGINS . '/system/mokosuite/Helper/MokoSuiteHelper.php';
if (file_exists($helperFile)) if (file_exists($helperFile))
{ {
require_once $helperFile; require_once $helperFile;
if (method_exists(\Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper::class, 'getMasterUsernames')) if (method_exists(\Moko\Plugin\System\MokoSuite\Helper\MokoSuiteHelper::class, 'getMasterUsernames'))
{ {
return \Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper::getMasterUsernames(); return \Moko\Plugin\System\MokoSuite\Helper\MokoSuiteHelper::getMasterUsernames();
} }
} }
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -18,7 +18,7 @@ use Joomla\Registry\Registry;
/** /**
* Demo site reset API controller. * Demo site reset API controller.
* *
* POST /api/index.php/v1/mokowaas/reset * POST /api/index.php/v1/mokosuite/reset
* Body: {"baseline": "default"} * Body: {"baseline": "default"}
* *
* Restores the site to a named baseline snapshot. * Restores the site to a named baseline snapshot.
@@ -53,11 +53,11 @@ class ResetController extends BaseController
return; return;
} }
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
if (!$plugin) if (!$plugin)
{ {
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']); $this->sendJson(503, ['error' => 'MokoSuite system plugin not enabled']);
return; return;
} }
@@ -84,13 +84,13 @@ class ResetController extends BaseController
* *
* @param Registry $params Plugin parameters * @param Registry $params Plugin parameters
* *
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService * @return \Moko\Plugin\System\MokoSuite\Service\DemoResetService
* *
* @since 02.21.00 * @since 02.21.00
*/ */
private function createService(Registry $params) private function createService(Registry $params)
{ {
$serviceFile = JPATH_PLUGINS . '/task/mokowaasdemo/src/Service/DemoResetService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokosuitedemo/src/Service/DemoResetService.php';
if (!file_exists($serviceFile)) if (!file_exists($serviceFile))
{ {
@@ -101,7 +101,7 @@ class ResetController extends BaseController
$media = (bool) $params->get('demo_snapshot_include_media', 1); $media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\Task\MokoWaaSDemo\Service\DemoResetService($media); return new \Moko\Plugin\Task\MokoSuiteDemo\Service\DemoResetService($media);
} }
/** /**
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -18,8 +18,8 @@ use Joomla\Registry\Registry;
/** /**
* Snapshot management API controller. * Snapshot management API controller.
* *
* GET /api/index.php/v1/mokowaas/snapshot list snapshots * GET /api/index.php/v1/mokosuite/snapshot list snapshots
* POST /api/index.php/v1/mokowaas/snapshot create snapshot * POST /api/index.php/v1/mokosuite/snapshot create snapshot
* *
* @since 02.21.00 * @since 02.21.00
*/ */
@@ -88,7 +88,7 @@ class SnapshotController extends BaseController
try try
{ {
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
$params = $plugin ? new Registry($plugin->params) : new Registry; $params = $plugin ? new Registry($plugin->params) : new Registry;
$body = json_decode($app->input->json->getRaw(), true); $body = json_decode($app->input->json->getRaw(), true);
@@ -112,13 +112,13 @@ class SnapshotController extends BaseController
/** /**
* Create DemoResetService from plugin params. * Create DemoResetService from plugin params.
* *
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService * @return \Moko\Plugin\System\MokoSuite\Service\DemoResetService
* *
* @since 02.21.00 * @since 02.21.00
*/ */
private function createService() private function createService()
{ {
$serviceFile = JPATH_PLUGINS . '/task/mokowaasdemo/src/Service/DemoResetService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokosuitedemo/src/Service/DemoResetService.php';
if (!file_exists($serviceFile)) if (!file_exists($serviceFile))
{ {
@@ -127,12 +127,12 @@ class SnapshotController extends BaseController
require_once $serviceFile; require_once $serviceFile;
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
$params = $plugin ? new Registry($plugin->params) : new Registry; $params = $plugin ? new Registry($plugin->params) : new Registry;
$media = (bool) $params->get('demo_snapshot_include_media', 1); $media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\Task\MokoWaaSDemo\Service\DemoResetService($media); return new \Moko\Plugin\Task\MokoSuiteDemo\Service\DemoResetService($media);
} }
/** /**
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -18,7 +18,7 @@ use Joomla\Registry\Registry;
/** /**
* Content sync trigger API controller (sender side). * Content sync trigger API controller (sender side).
* *
* POST /api/index.php/v1/mokowaas/sync * POST /api/index.php/v1/mokosuite/sync
* *
* Pushes content to all configured sync targets. * Pushes content to all configured sync targets.
* *
@@ -44,11 +44,11 @@ class SyncController extends BaseController
return; return;
} }
$plugin = PluginHelper::getPlugin('system', 'mokowaas'); $plugin = PluginHelper::getPlugin('system', 'mokosuite');
if (!$plugin) if (!$plugin)
{ {
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']); $this->sendJson(503, ['error' => 'MokoSuite system plugin not enabled']);
return; return;
} }
@@ -57,10 +57,10 @@ class SyncController extends BaseController
$params = new Registry($plugin->params); $params = new Registry($plugin->params);
$targets = json_decode($params->get('sync_targets', '[]'), true) ?: []; $targets = json_decode($params->get('sync_targets', '[]'), true) ?: [];
$serviceFile = JPATH_PLUGINS . '/task/mokowaassync/src/Service/ContentSyncService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokosuitesync/src/Service/ContentSyncService.php';
require_once $serviceFile; require_once $serviceFile;
$service = new \Moko\Plugin\Task\MokoWaaSSync\Service\ContentSyncService(); $service = new \Moko\Plugin\Task\MokoSuiteSync\Service\ContentSyncService();
$result = $service->syncAllTargets($targets); $result = $service->syncAllTargets($targets);
$this->sendJson(200, $result); $this->sendJson(200, $result);
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Content sync receiver API controller (target side). * Content sync receiver API controller (target side).
* *
* POST /api/index.php/v1/mokowaas/sync-receive * POST /api/index.php/v1/mokosuite/sync-receive
* *
* Accepts a JSON payload from a source site and applies the content locally. * Accepts a JSON payload from a source site and applies the content locally.
* *
@@ -46,16 +46,16 @@ class SyncReceiveController extends BaseController
{ {
$payload = json_decode($app->input->json->getRaw(), true); $payload = json_decode($app->input->json->getRaw(), true);
if (empty($payload['mokowaas_sync'])) if (empty($payload['mokosuite_sync']))
{ {
$this->sendJson(400, ['error' => 'Invalid payload — missing mokowaas_sync version']); $this->sendJson(400, ['error' => 'Invalid payload — missing mokosuite_sync version']);
return; return;
} }
$serviceFile = JPATH_PLUGINS . '/task/mokowaassync/src/Service/ContentSyncReceiver.php'; $serviceFile = JPATH_PLUGINS . '/task/mokosuitesync/src/Service/ContentSyncReceiver.php';
require_once $serviceFile; require_once $serviceFile;
$receiver = new \Moko\Plugin\Task\MokoWaaSSync\Service\ContentSyncReceiver(); $receiver = new \Moko\Plugin\Task\MokoSuiteSync\Service\ContentSyncReceiver();
$result = $receiver->receive($payload); $result = $receiver->receive($payload);
$this->sendJson(200, $result); $this->sendJson(200, $result);
@@ -0,0 +1,261 @@
<?php
/**
* @package MokoSuite
* @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuite\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/mokosuite/tickets - list tickets
* GET /api/index.php/v1/mokosuite/tickets/{id} - get single ticket with replies
* POST /api/index.php/v1/mokosuite/tickets - create ticket
* PATCH /api/index.php/v1/mokosuite/tickets/{id} - update ticket fields
* POST /api/index.php/v1/mokosuite/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_mokosuite');
$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('#__mokosuite_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuite_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
$countQuery = $db->getQuery(true)->select('COUNT(*)')->from('#__mokosuite_tickets');
$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_mokosuite');
$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('#__mokosuite_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuite_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('#__mokosuite_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\MokoSuite\Administrator\Service\AttachmentService::getForTicket($id);
$this->sendJson(200, $ticket);
}
/**
* POST /tickets — create a new ticket.
*/
public function create(): void
{
$this->requireAuth('core.manage', 'com_mokosuite');
$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;
}
$ticket = (object) [
'subject' => $subject,
'body' => $body,
'status' => 'open',
'status_id' => $input->getInt('status_id', 0) ?: null,
'priority' => $input->getString('priority', 'normal'),
'priority_id' => $input->getInt('priority_id', 0) ?: null,
'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('#__mokosuite_tickets', $ticket, 'id');
// Trigger notification
\Moko\Component\MokoSuite\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_mokosuite');
$input = Factory::getApplication()->getInput();
$id = $input->getInt('id', 0);
$db = Factory::getDbo();
$fields = [];
$updatable = ['status', 'status_id', 'priority', 'priority_id', 'category_id', 'assigned_to'];
foreach ($updatable as $field) {
$value = $input->get($field, null, 'raw');
if ($value !== null) {
$fields[$field] = $value;
}
}
if (empty($fields)) {
$this->sendJson(400, ['error' => 'No fields to update']);
return;
}
$sets = [];
foreach ($fields as $k => $v) {
$sets[] = $db->quoteName($k) . ' = ' . $db->quote($v);
}
$sets[] = 'modified = ' . $db->quote(Factory::getDate()->toSql());
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuite_tickets') . ' SET ' . implode(', ', $sets) . ' WHERE id = ' . $id)->execute();
$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_mokosuite');
$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('#__mokosuite_ticket_replies', $reply, 'id');
// Notify
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuite_tickets')->where('id = ' . $ticketId));
$ticket = $db->loadObject();
if ($ticket) {
\Moko\Component\MokoSuite\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']);
}
}
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();
}
}
@@ -1,12 +1,12 @@
<?php <?php
/** /**
* @package MokoWaaS * @package MokoSuite
* @subpackage com_mokowaas * @subpackage com_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Moko\Component\MokoWaaS\Api\Controller; namespace Moko\Component\MokoSuite\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -16,7 +16,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
/** /**
* Update check API controller. * Update check API controller.
* *
* POST /api/index.php/v1/mokowaas/update * POST /api/index.php/v1/mokosuite/update
* *
* @since 1.0.0 * @since 1.0.0
*/ */
@@ -1,50 +1,50 @@
/** /**
* MokoWaaS Dashboard Styles * MokoSuite Dashboard Styles
* @package com_mokowaas * @package com_mokosuite
*/ */
/* Info bar */ /* Info bar */
.mokowaas-info-bar .card-body { .mokosuite-info-bar .card-body {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
.mokowaas-info-item { .mokosuite-info-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.mokowaas-info-label { .mokosuite-info-label {
font-size: 0.8125rem; font-size: 0.8125rem;
color: #6c757d; color: #6c757d;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.025em; letter-spacing: 0.025em;
} }
.mokowaas-info-value { .mokosuite-info-value {
font-size: 0.875rem; font-size: 0.875rem;
} }
/* Plugin cards */ /* Plugin cards */
.mokowaas-plugin-card { .mokosuite-plugin-card {
transition: box-shadow 0.15s ease, opacity 0.15s ease; transition: box-shadow 0.15s ease, opacity 0.15s ease;
border-left: 3px solid #0d6efd; border-left: 3px solid #0d6efd;
} }
.mokowaas-plugin-card:hover { .mokosuite-plugin-card:hover {
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
} }
.mokowaas-plugin-disabled { .mokosuite-plugin-disabled {
opacity: 0.6; opacity: 0.6;
border-left-color: #adb5bd; border-left-color: #adb5bd;
} }
.mokowaas-plugin-disabled:hover { .mokosuite-plugin-disabled:hover {
opacity: 0.8; opacity: 0.8;
} }
.mokowaas-plugin-icon { .mokosuite-plugin-icon {
font-size: 1.5rem; font-size: 1.5rem;
color: #1a2744; color: #1a2744;
width: 2rem; width: 2rem;
@@ -52,37 +52,37 @@
} }
/* Category headings */ /* Category headings */
.mokowaas-category-heading { .mokosuite-category-heading {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
padding-top: 0.5rem; padding-top: 0.5rem;
} }
/* Toggle switch */ /* Toggle switch */
.mokowaas-toggle { .mokosuite-toggle {
cursor: pointer; cursor: pointer;
} }
.mokowaas-toggle:disabled { .mokosuite-toggle:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
/* Quick actions */ /* Quick actions */
.mokowaas-quick-actions .btn { .mokosuite-quick-actions .btn {
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.mokowaas-quick-actions .btn:disabled { .mokosuite-quick-actions .btn:disabled {
pointer-events: none; pointer-events: none;
} }
/* Loading spinner overlay on toggle */ /* Loading spinner overlay on toggle */
.mokowaas-plugin-card.is-loading { .mokosuite-plugin-card.is-loading {
position: relative; position: relative;
pointer-events: none; pointer-events: none;
} }
.mokowaas-plugin-card.is-loading::after { .mokosuite-plugin-card.is-loading::after {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -0,0 +1,38 @@
/**
* MokoSuite+ERP Customer Portal styles
* @since 02.34.16
*/
.mokosuite-portal h2,
.mokosuite-portal-orders h2,
.mokosuite-portal-invoices h2,
.mokosuite-portal-license h2 {
color: #1a2744;
font-weight: 700;
}
/* Signing page */
.mokosuite-sign-page {
max-width: 800px;
margin: 0 auto;
}
#signature-canvas {
border: 1px solid #dee2e6;
border-radius: 4px;
background: #fff;
}
/* Verification page */
.mokosuite-verify-page {
max-width: 900px;
margin: 0 auto;
}
/* Portal cards */
.mokosuite-portal .card {
transition: box-shadow 0.15s;
}
.mokosuite-portal .card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
@@ -1,16 +1,16 @@
/** /**
* MokoWaaS Dashboard Scripts * MokoSuite Dashboard Scripts
* @package com_mokowaas * @package com_mokosuite
*/ */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
'use strict'; 'use strict';
// Plugin toggle switches // Plugin toggle switches
document.querySelectorAll('.mokowaas-toggle').forEach(function (toggle) { document.querySelectorAll('.mokosuite-toggle').forEach(function (toggle) {
toggle.addEventListener('change', function () { toggle.addEventListener('change', function () {
var checkbox = this; var checkbox = this;
var card = checkbox.closest('.mokowaas-plugin-card'); var card = checkbox.closest('.mokosuite-plugin-card');
var extensionId = checkbox.dataset.extensionId; var extensionId = checkbox.dataset.extensionId;
var url = checkbox.dataset.url; var url = checkbox.dataset.url;
var token = checkbox.dataset.token; var token = checkbox.dataset.token;
@@ -35,11 +35,11 @@ document.addEventListener('DOMContentLoaded', function () {
.then(function (response) { return response.json(); }) .then(function (response) { return response.json(); })
.then(function (data) { .then(function (data) {
if (data.success) { if (data.success) {
card.classList.toggle('mokowaas-plugin-disabled', !enabled); card.classList.toggle('mokosuite-plugin-disabled', !enabled);
if (label) { if (label) {
label.textContent = enabled label.textContent = enabled
? Joomla.Text._('COM_MOKOWAAS_ENABLED') || 'Enabled' ? Joomla.Text._('COM_MOKOSUITE_ENABLED') || 'Enabled'
: Joomla.Text._('COM_MOKOWAAS_DISABLED') || 'Disabled'; : Joomla.Text._('COM_MOKOSUITE_DISABLED') || 'Disabled';
} }
} else { } else {
// Revert on failure // Revert on failure
@@ -59,7 +59,7 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
// Clear cache button // Clear cache button
var cacheBtn = document.getElementById('mokowaas-btn-cache'); var cacheBtn = document.getElementById('mokosuite-btn-cache');
if (cacheBtn) { if (cacheBtn) {
cacheBtn.addEventListener('click', function () { cacheBtn.addEventListener('click', function () {
var btn = this; var btn = this;
@@ -116,7 +116,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (!btn) return; if (!btn) return;
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var el = this; var el = this;
if (!confirm('Import Akeeba data into MokoWaaS? Akeeba extensions will be disabled after import.')) return; if (!confirm('Import Akeeba data into MokoSuite? Akeeba extensions will be disabled after import.')) return;
el.disabled = true; el.disabled = true;
var origText = el.textContent; var origText = el.textContent;
el.textContent = ' Importing...'; el.textContent = ' Importing...';
@@ -0,0 +1,162 @@
/**
* MokoSuite+ERP Signature Pad — HTML5 Canvas drawing for e-signature capture.
* Touch-friendly, works on mobile/tablet/desktop.
* @since 02.34.16
*/
document.addEventListener('DOMContentLoaded', function () {
'use strict';
var canvas = document.getElementById('signature-canvas');
if (!canvas) { return; }
var ctx = canvas.getContext('2d');
var drawing = false;
var hasSigned = false;
// High-DPI support
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#000';
function getPos(e) {
var r = canvas.getBoundingClientRect();
var touch = e.touches ? e.touches[0] : e;
return { x: touch.clientX - r.left, y: touch.clientY - r.top };
}
canvas.addEventListener('mousedown', function (e) { drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); });
canvas.addEventListener('mousemove', function (e) { if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSigned = true; });
canvas.addEventListener('mouseup', function () { drawing = false; });
canvas.addEventListener('mouseleave', function () { drawing = false; });
canvas.addEventListener('touchstart', function (e) { e.preventDefault(); drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); }, { passive: false });
canvas.addEventListener('touchmove', function (e) { e.preventDefault(); if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSigned = true; }, { passive: false });
canvas.addEventListener('touchend', function () { drawing = false; });
// Clear
var clearBtn = document.getElementById('clear-signature');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hasSigned = false;
});
}
// Submit
var form = document.getElementById('signing-form');
if (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!hasSigned) {
alert('Please draw your signature before submitting.');
return;
}
var consentBox = document.getElementById('consent-checkbox');
if (consentBox && !consentBox.checked) {
alert('You must accept the e-signature consent agreement.');
return;
}
var token = form.dataset.token;
var signatureData = canvas.toDataURL('image/png');
var basePath = (Joomla.getOptions('system.paths') || {}).baseFull || '';
var body = {
token: token,
signature: signatureData,
signature_type: 'draw'
};
// Geolocation
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (pos) {
body.geo_lat = pos.coords.latitude;
body.geo_lon = pos.coords.longitude;
submitSignature(basePath, body);
}, function () {
submitSignature(basePath, body);
}, { timeout: 5000 });
} else {
submitSignature(basePath, body);
}
});
}
function submitSignature(basePath, body) {
var btn = document.getElementById('btn-sign');
btn.disabled = true;
btn.textContent = 'Submitting...';
// If consent needed, send consent first
var consentBox = document.getElementById('consent-checkbox');
var consentPromise = Promise.resolve();
if (consentBox) {
consentPromise = fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: body.token, accepted: true, action: 'consent' })
}).then(function (r) { return r.json(); });
}
consentPromise.then(function () {
return fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.ok) {
document.querySelector('.mokosuite-sign-page').textContent = '';
var success = document.createElement('div');
success.className = 'alert alert-success fs-5 text-center py-5';
success.textContent = 'Document signed successfully. Thank you!';
document.querySelector('.mokosuite-sign-page').appendChild(success);
} else {
alert(result.error || 'Signing failed. Please try again.');
btn.disabled = false;
btn.textContent = 'Sign Document';
}
})
.catch(function (err) {
alert('Network error: ' + err.message);
btn.disabled = false;
btn.textContent = 'Sign Document';
});
}
// Decline
var declineBtn = document.getElementById('btn-decline');
if (declineBtn) {
declineBtn.addEventListener('click', function () {
var reason = prompt('Reason for declining (optional):');
if (reason === null) { return; }
var token = form.dataset.token;
var basePath = (Joomla.getOptions('system.paths') || {}).baseFull || '';
fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token, reason: reason, action: 'decline' })
})
.then(function (r) { return r.json(); })
.then(function (result) {
document.querySelector('.mokosuite-sign-page').textContent = '';
var msg = document.createElement('div');
msg.className = 'alert alert-warning fs-5 text-center py-5';
msg.textContent = 'Document declined.';
document.querySelector('.mokosuite-sign-page').appendChild(msg);
});
});
}
});
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: Joomla.Component
INGROUP: MokoSuite
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
VERSION: 02.34.16
PATH: /mokosuite.xml
BRIEF: Component manifest for MokoSuite admin dashboard and REST API
-->
<extension type="component" method="upgrade">
<name>MokoSuite</name>
<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.34.79-dev</version>
<description>MokoSuite admin dashboard and REST API. Provides a control panel for managing MokoSuite feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoSuite</namespace>
<install>
<sql><file driver="mysql" charset="utf8">sql/install.mysql.sql</file></sql>
</install>
<uninstall>
<sql><file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file></sql>
</uninstall>
<update>
<schemas>
<schemapath type="mysql">sql/updates/mysql</schemapath>
</schemas>
</update>
<administration>
<menu img="class:cogs">MokoSuite</menu>
<submenu>
<menu link="option=com_mokosuite" img="class:cogs">COM_MOKOSUITE_MENU_DASHBOARD</menu>
<menu link="option=com_mokosuite&amp;view=extensions" img="class:puzzle-piece">COM_MOKOSUITE_MENU_EXTENSIONS</menu>
<menu link="option=com_mokosuite&amp;view=tickets" img="class:headphones">COM_MOKOSUITE_MENU_TICKETS</menu>
<menu link="option=com_mokosuite&amp;view=htaccess" img="class:file-code">COM_MOKOSUITE_MENU_HTACCESS</menu>
<menu link="option=com_mokosuite&amp;view=privacy" img="class:lock">COM_MOKOSUITE_MENU_PRIVACY</menu>
<menu link="option=com_mokosuite&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITE_MENU_WAFLOG</menu>
<menu link="option=com_mokosuite&amp;view=database" img="class:database">COM_MOKOSUITE_MENU_DATABASE</menu>
<menu link="option=com_mokosuite&amp;view=cleanup" img="class:trash">COM_MOKOSUITE_MENU_CLEANUP</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokosuite" img="class:power-off">COM_MOKOSUITE_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOSUITE_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITE_MENU_CHECKIN</menu>
<menu link="option=com_cache" img="class:bolt">COM_MOKOSUITE_MENU_CACHE</menu>
</submenu>
<files folder="admin">
<filename>access.xml</filename>
<filename>catalog.xml</filename>
<filename>config.xml</filename>
<folder>language</folder>
<folder>services</folder>
<folder>sql</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="admin/language">
<language tag="en-GB">en-GB/com_mokosuite.sys.ini</language>
</languages>
</administration>
<files folder="site">
<folder>language</folder>
<folder>services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<install>
<sql><file driver="mysql" charset="utf8">admin/sql/install.mysql.sql</file></sql>
</install>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
<media destination="com_mokosuite" folder="media">
<folder>css</folder>
<folder>js</folder>
</media>
</extension>
@@ -0,0 +1,11 @@
; MokoSuite Customer Portal - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITE_PORTAL_TITLE="Support Portal"
COM_MOKOSUITE_PORTAL_MY_TICKETS="My Support Tickets"
COM_MOKOSUITE_PORTAL_NEW_TICKET="New Ticket"
COM_MOKOSUITE_PORTAL_SUBMIT="Submit Ticket"
COM_MOKOSUITE_PORTAL_REPLY="Send Reply"
COM_MOKOSUITE_PORTAL_NO_TICKETS="You haven't submitted any support tickets yet."
COM_MOKOSUITE_PORTAL_LOGIN_REQUIRED="Please log in to access the support portal."

Some files were not shown because too many files have changed in this diff Show More