Compare commits

..

78 Commits

Author SHA1 Message Date
gitea-actions[bot] 1736407c59 chore(version): pre-release bump to 02.34.45-dev [skip ci] 2026-06-07 03:43:07 +00:00
gitea-actions[bot] 7c2dda36d9 chore(version): pre-release bump to 02.34.44-dev [skip ci] 2026-06-07 03:42:35 +00: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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 27s
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) 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
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 / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 29s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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) 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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 33s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 29s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
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) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 29s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
- 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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
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) 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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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) 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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
92 changed files with 2329 additions and 8274 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
+1 -5
View File
@@ -9,11 +9,7 @@
<display-name>Package - MokoWaaS</display-name> <display-name>Package - MokoWaaS</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 WaaS-managed Joomla environments</description>
<<<<<<< HEAD <version>02.34.45</version>
<version>02.34.00</version>
=======
<version>02.34.16</version>
>>>>>>> origin/dev
<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>
+324 -316
View File
@@ -1,316 +1,324 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +========================================================================+ # +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+ # +========================================================================+
# | | # | |
# | 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 |
# | | # | |
# +========================================================================+ # +========================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [opened, closed] types: [opened, closed]
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
action: action:
description: 'Action to perform' description: 'Action to perform'
required: false required: false
type: choice type: choice
default: release default: release
options: options:
- release - release
- promote-rc - promote-rc
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions: permissions:
contents: write contents: write
jobs: jobs:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────── # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
if: >- if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) || (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup moko-platform tools - name: Setup moko-platform tools
env: env:
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
fi echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
rm -rf /tmp/moko-platform-api else
git clone --depth 1 --branch main --quiet \ echo Falling back to fresh clone
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ if ! command -v composer > /dev/null 2>&1; then
/tmp/moko-platform-api 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
cd /tmp/moko-platform-api fi
composer install --no-dev --no-interaction --quiet rm -rf /tmp/moko-platform-api
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" 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
- name: Rename branch to rc cd /tmp/moko-platform-api
run: | composer install --no-dev --no-interaction --quiet
php ${MOKO_CLI}/branch_rename.php \ echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ fi
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - name: Rename branch to rc
--pr "${{ github.event.pull_request.number }}" run: |
php ${MOKO_CLI}/branch_rename.php \
- name: Checkout rc and configure git --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
run: | --token "${{ secrets.MOKOGITEA_TOKEN }}" \
git fetch origin rc --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
git checkout rc --pr "${{ github.event.pull_request.number }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" - name: Checkout rc and configure git
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" run: |
git fetch origin rc
- name: Publish RC release git checkout rc
run: | git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
php ${MOKO_CLI}/release_publish.php \ git config --local user.name "gitea-actions[bot]"
--path . --stability rc --bump minor --branch rc \ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream - name: Publish RC release
run: |
- name: Summary php ${MOKO_CLI}/release_publish.php \
if: always() --path . --stability rc --bump minor --branch rc \
run: | --token "${{ secrets.MOKOGITEA_TOKEN }}"
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 - name: Summary
if: always()
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── run: |
release: echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
name: Build & Release Pipeline echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
runs-on: release
if: >- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
github.event.pull_request.merged == true || release:
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') name: Build & Release Pipeline
runs-on: release
steps: if: >-
- name: Checkout repository github.event.pull_request.merged == true ||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
with:
token: ${{ secrets.MOKOGITEA_TOKEN }} steps:
fetch-depth: 0 - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Configure git for bot pushes with:
run: | token: ${{ secrets.MOKOGITEA_TOKEN }}
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" fetch-depth: 0
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - name: Configure git for bot pushes
run: |
- name: Check for merge conflict markers git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
run: | git config --local user.name "gitea-actions[bot]"
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) git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found - aborting release" - name: Check for merge conflict markers
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY run: |
echo '```' >> $GITHUB_STEP_SUMMARY 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)
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY if [ -n "$CONFLICTS" ]; then
echo '```' >> $GITHUB_STEP_SUMMARY echo "::error::Merge conflict markers found — aborting release"
exit 1 echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
fi echo '```' >> $GITHUB_STEP_SUMMARY
echo "No conflict markers found" echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Setup moko-platform tools exit 1
env: fi
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} echo "No conflict markers found"
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - name: Setup moko-platform tools
run: | env:
if ! command -v composer &> /dev/null; then MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
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 MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
fi COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
rm -rf /tmp/moko-platform-api run: |
git clone --depth 1 --branch main --quiet \ if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ echo Using pre-installed /opt/moko-platform
/tmp/moko-platform-api echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
cd /tmp/moko-platform-api else
composer install --no-dev --no-interaction --quiet echo Falling back to fresh clone
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" 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
- name: "Publish stable release" fi
run: | rm -rf /tmp/moko-platform-api
php ${MOKO_CLI}/release_publish.php \ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
--path . --stability stable --bump minor --branch main \ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ cd /tmp/moko-platform-api
--skip-update-stream composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
- name: Update release notes from CHANGELOG.md fi
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - name: "Publish stable release"
run: |
# Extract [Unreleased] section from changelog php ${MOKO_CLI}/release_publish.php \
if [ -f "CHANGELOG.md" ]; then --path . --stability stable --bump minor --branch main \
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) --token "${{ secrets.MOKOGITEA_TOKEN }}"
[ -z "$NOTES" ] && NOTES="Stable release"
else - name: Update release notes from CHANGELOG.md
NOTES="Stable release" run: |
fi API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Update release body via API # Extract [Unreleased] section from changelog
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ if [ -f "CHANGELOG.md" ]; then
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
if [ -n "$RELEASE_ID" ]; then else
python3 -c " NOTES="Stable release"
import json, urllib.request fi
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode() # Update release body via API
req = urllib.request.Request( RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
'${API_BASE}/releases/${RELEASE_ID}', "${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
data=payload, method='PATCH',
headers={ if [ -n "$RELEASE_ID" ]; then
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', python3 -c "
'Content-Type': 'application/json' import json, urllib.request
}) body = open('/dev/stdin').read()
urllib.request.urlopen(req) payload = json.dumps({'body': body}).encode()
" <<< "$NOTES" req = urllib.request.Request(
echo "Release notes updated from CHANGELOG.md" '${API_BASE}/releases/${RELEASE_ID}',
fi data=payload, method='PATCH',
headers={
# -- STEP 9: Mirror to GitHub (stable only) -------------------------------- 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
- name: "Step 9: Mirror release to GitHub" 'Content-Type': 'application/json'
if: >- })
steps.version.outputs.skip != 'true' && urllib.request.urlopen(req)
secrets.GH_MIRROR_TOKEN != '' " <<< "$NOTES"
continue-on-error: true echo "Release notes updated from CHANGELOG.md"
run: | fi
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - name: "Step 9: Mirror release to GitHub"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" if: >-
php ${MOKO_CLI}/release_mirror.php \ steps.version.outputs.skip != 'true' &&
--version "$VERSION" --tag "$RELEASE_TAG" \ secrets.GH_MIRROR_TOKEN != ''
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ continue-on-error: true
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ run: |
--branch main 2>&1 || true VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
# -- STEP 10: Sync main branch to GitHub mirror ---------------------------- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- name: "Step 10: Push main to GitHub mirror" php ${MOKO_CLI}/release_mirror.php \
if: >- --version "$VERSION" --tag "$RELEASE_TAG" \
steps.version.outputs.skip != 'true' && --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
secrets.GH_MIRROR_TOKEN != '' --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
continue-on-error: true --branch main 2>&1 || true
run: | echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - name: "Step 10: Push main to GitHub mirror"
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ if: >-
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" steps.version.outputs.skip != 'true' &&
git fetch origin main --depth=1 secrets.GH_MIRROR_TOKEN != ''
git push github origin/main:refs/heads/main --force 2>/dev/null \ continue-on-error: true
&& echo "main branch pushed to GitHub mirror" \ run: |
|| echo "WARNING: GitHub mirror push failed" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- name: "Step 11: Delete rc branch and recreate dev from main" GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
if: steps.version.outputs.skip != 'true' git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
continue-on-error: true git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
run: | git fetch origin main --depth=1
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" git push github origin/main:refs/heads/main --force 2>/dev/null \
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" && echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# Delete rc branch (ephemeral - created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - name: "Step 11: Delete rc branch and recreate dev from main"
"${API_BASE}/branches/rc" 2>/dev/null \ if: steps.version.outputs.skip != 'true'
&& echo "Deleted rc branch" || echo "rc branch not found" continue-on-error: true
run: |
# Delete dev branch API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Delete rc branch (ephemeral — created by promote-rc)
# Recreate dev from main (now includes version bump + changelog promotion) curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
curl -sf -X POST -H "Authorization: token ${TOKEN}" \ "${API_BASE}/branches/rc" 2>/dev/null \
-H "Content-Type: application/json" \ && echo "Deleted rc branch" || echo "rc branch not found"
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" # Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
- name: "Step 12: Create version branch from main" # Recreate dev from main (now includes version bump + changelog promotion)
if: steps.version.outputs.skip != 'true' curl -sf -X POST -H "Authorization: token ${TOKEN}" \
continue-on-error: true -H "Content-Type: application/json" \
run: | "${API_BASE}/branches" \
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD) - name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
# Delete old version branch if it exists (same version re-release) continue-on-error: true
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create version/XX.YY.ZZ from main TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version" # Create version/XX.YY.ZZ from main
if: steps.version.outputs.skip != 'true' curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
continue-on-error: true
run: | echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true # -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
# -- Summary -------------------------------------------------------------- if: steps.version.outputs.skip != 'true'
- name: Pipeline Summary continue-on-error: true
if: always() run: |
run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" php ${MOKO_CLI}/version_reset_dev.php \
PLATFORM="${{ steps.platform.outputs.platform }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then --branch dev --path . 2>&1 || true
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY # -- Summary --------------------------------------------------------------
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - name: Pipeline Summary
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY if: always()
else run: |
echo "" >> $GITHUB_STEP_SUMMARY VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY PLATFORM="${{ steps.platform.outputs.platform }}"
echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY else
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
fi echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+1 -5
View File
@@ -5,11 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation # INGROUP: moko-platform.Automation
<<<<<<< HEAD # VERSION: 02.34.45
# VERSION: 02.34.00
=======
# VERSION: 02.34.16
>>>>>>> origin/dev
# 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"
+243 -241
View File
@@ -1,241 +1,243 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release # INGROUP: moko-platform.Release
# 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: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
pull_request: pull_request:
types: [closed] types: [closed]
branches: branches:
- dev - dev
pull_request_target: pull_request_target:
types: [synchronize, opened, reopened] types: [synchronize, opened, reopened]
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
description: 'Pre-release channel' description: 'Pre-release channel'
required: true required: true
type: choice type: choice
options: options:
- development - development
- alpha - alpha
- beta - beta
- release-candidate - release-candidate
permissions: permissions:
contents: write contents: write
env: env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})" name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
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 == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
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.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools - name: Setup moko-platform tools
env: env:
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: |
# 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: | 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: RC for PRs targeting main, else use input or default to development if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then STABILITY="release-candidate"
STABILITY="release-candidate" else
else STABILITY="${{ inputs.stability || 'development' }}"
STABILITY="${{ inputs.stability || 'development' }}" fi
fi
case "$STABILITY" in
case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;;
development) SUFFIX="-dev"; TAG="development" ;; alpha) SUFFIX="-alpha"; TAG="alpha" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;; beta) SUFFIX="-beta"; TAG="beta" ;;
beta) SUFFIX="-beta"; TAG="beta" ;; release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; esac
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
# Bump version via CLI: patch for dev/alpha/beta, minor for RC case "$STABILITY" in
case "$STABILITY" in release-candidate) BUMP="minor" ;;
release-candidate) BUMP="minor" ;; *) BUMP="patch" ;;
*) BUMP="patch" ;; esac
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
# Set stability suffix and verify consistency VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
php ${MOKO_CLI}/version_set_platform.php \ --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
# Append suffix for output php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}" # Append suffix for output
fi if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
# Commit version bump fi
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" # Commit version bump
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git add -A git config --local user.name "gitea-actions[bot]"
git diff --cached --quiet || { git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" git add -A
git push origin HEAD 2>&1 git diff --cached --quiet || {
} git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
# Auto-detect element via manifest_element.php }
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \ # Auto-detect element via manifest_element.php
--repo "${GITEA_REPO}" --github-output php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
# Read back element outputs --repo "${GITEA_REPO}" --github-output
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) # Read back element outputs
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
- name: Create release echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
id: release
run: | - name: Create release
TAG="${{ steps.meta.outputs.tag }}" id: release
VERSION="${{ steps.meta.outputs.version }}" run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TAG="${{ steps.meta.outputs.tag }}"
php ${MOKO_CLI}/release_create.php \ VERSION="${{ steps.meta.outputs.version }}"
--path . --version "$VERSION" --tag "$TAG" \ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ php ${MOKO_CLI}/release_create.php \
--repo "${GITEA_REPO}" --branch dev --prerelease --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- name: Update release notes from CHANGELOG.md --repo "${GITEA_REPO}" --branch dev --prerelease
run: |
TAG="${{ steps.meta.outputs.tag }}" - name: Update release notes from CHANGELOG.md
VERSION="${{ steps.meta.outputs.version }}" run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}" if [ -f "CHANGELOG.md" ]; then
else NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
NOTES="Release ${VERSION}" [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
fi else
NOTES="Release ${VERSION}"
# Update release body via API fi
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) # Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
if [ -n "$RELEASE_ID" ]; then "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
python3 -c "
import json, urllib.request if [ -n "$RELEASE_ID" ]; then
body = open('/dev/stdin').read() python3 -c "
payload = json.dumps({'body': body}).encode() import json, urllib.request
req = urllib.request.Request( body = open('/dev/stdin').read()
'${API_BASE}/releases/${RELEASE_ID}', payload = json.dumps({'body': body}).encode()
data=payload, method='PATCH', req = urllib.request.Request(
headers={ '${API_BASE}/releases/${RELEASE_ID}',
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', data=payload, method='PATCH',
'Content-Type': 'application/json' headers={
}) 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
urllib.request.urlopen(req) 'Content-Type': 'application/json'
" <<< "$NOTES" })
echo "Release notes updated from CHANGELOG.md" urllib.request.urlopen(req)
fi " <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
- name: Build package and upload fi
id: package
run: | - name: Build package and upload
VERSION="${{ steps.meta.outputs.version }}" id: package
TAG="${{ steps.meta.outputs.tag }}" run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" VERSION="${{ steps.meta.outputs.version }}"
php ${MOKO_CLI}/release_package.php \ TAG="${{ steps.meta.outputs.tag }}"
--path . --version "$VERSION" --tag "$TAG" \ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ php ${MOKO_CLI}/release_package.php \
--repo "${GITEA_REPO}" --output /tmp || true --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
# updates.xml is generated dynamically by MokoGitea license server --repo "${GITEA_REPO}" --output /tmp || true
# No need to build, commit, or sync updates.xml from workflows
# updates.xml is generated dynamically by MokoGitea license server
- name: "Delete lesser pre-release channels (cascade)" # No need to build, commit, or sync updates.xml from workflows
continue-on-error: true
run: | - name: "Delete lesser pre-release channels (cascade)"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" continue-on-error: true
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_cascade.php \ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \ php ${MOKO_CLI}/release_cascade.php \
--api-base "${API_BASE}" --stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
- name: Summary --api-base "${API_BASE}"
if: always()
run: | - name: Summary
VERSION="${{ steps.meta.outputs.version }}" if: always()
STABILITY="${{ steps.meta.outputs.stability }}" run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}" VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.package.outputs.sha256_zip }}" STABILITY="${{ steps.meta.outputs.stability }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
echo "" >> $GITHUB_STEP_SUMMARY SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+42 -5
View File
@@ -14,11 +14,7 @@
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md PATH: ./CHANGELOG.md
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
@@ -26,6 +22,47 @@
## [Unreleased] ## [Unreleased]
### Added
- RSA-signed heartbeat authentication — private key in monitor plugin manifest, public key on MokoWaaSHQ
- 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
### 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 MokoWaaSHQ 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_mokowaas_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 MokoWaaSHQ
- 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)
+1 -5
View File
@@ -14,11 +14,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
PATH: ./CODE_OF_CONDUCT.md PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
--> -->
+1 -5
View File
@@ -19,11 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaSBrand
--> -->
+1 -5
View File
@@ -15,11 +15,7 @@
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md PATH: ./LICENSE.md
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
BRIEF: Project license (GPL-3.0-or-later) BRIEF: Project license (GPL-3.0-or-later)
--> -->
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
+1 -5
View File
@@ -9,11 +9,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
PATH: /README.md PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla BRIEF: MokoWaaS platform plugin for Joomla
--> -->
+1 -5
View File
@@ -23,11 +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
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
+2 -10
View File
@@ -11,21 +11,13 @@
INGROUP: MokoWaaS.Build INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md FILE: build-guide.md
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
PATH: /docs/guides/ PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
--> -->
<<<<<<< HEAD # MokoWaaS Build Guide (VERSION: 02.34.45)
# MokoWaaS Build Guide (VERSION: 02.34.00)
=======
# MokoWaaS Build Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## 1. Purpose ## 1. Purpose
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
--> -->
<<<<<<< HEAD # MokoWaaS Configuration Guide (VERSION: 02.34.45)
# MokoWaaS Configuration Guide (VERSION: 02.34.00)
=======
# MokoWaaS Configuration Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## 1. Objective ## 1. Objective
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS system plugin
NOTE: First document in the guide set NOTE: First document in the guide set
--> -->
<<<<<<< HEAD # MokoWaaS Installation Guide (VERSION: 02.34.45)
# MokoWaaS Installation Guide (VERSION: 02.34.00)
=======
# MokoWaaS Installation Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors NOTE: Defines lifecycle, responsibilities, and operational behaviors
--> -->
<<<<<<< HEAD # MokoWaaS Operations Guide (VERSION: 02.34.45)
# MokoWaaS Operations Guide (VERSION: 02.34.00)
=======
# MokoWaaS Operations Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 WaaS plugin governance
--> -->
<<<<<<< HEAD # MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.45)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.00)
=======
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -7,21 +7,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
PATH: /docs/guides/testing-guide.md PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08 BRIEF: Testing guide for MokoWaaS 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
--> -->
<<<<<<< HEAD # MokoWaaS Testing Guide (VERSION: 02.34.45)
# MokoWaaS Testing Guide (VERSION: 02.34.00)
=======
# MokoWaaS Testing Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## 1. Prerequisites ## 1. Prerequisites
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams NOTE: Designed for administrators and WaaS operations teams
--> -->
<<<<<<< HEAD # MokoWaaS Troubleshooting Guide (VERSION: 02.34.45)
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.00)
=======
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation NOTE: Defines release flow, version rules, and upgrade validation
--> -->
<<<<<<< HEAD # MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.45)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.00)
=======
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -10,21 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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 MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases NOTE: Automatically maintained index for all guide canvases
--> -->
<<<<<<< HEAD # MokoWaaS Documentation Index (VERSION: 02.34.45)
# MokoWaaS Documentation Index (VERSION: 02.34.00)
=======
# MokoWaaS Documentation Index (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+2 -10
View File
@@ -11,20 +11,12 @@
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md PATH: /docs/plugin-basic.md
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
BRIEF: Baseline documentation for the MokoWaaS system plugin BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders NOTE: Foundational reference for internal and external stakeholders
--> -->
<<<<<<< HEAD # MokoWaaS Plugin Overview (VERSION: 02.34.45)
# MokoWaaS Plugin Overview (VERSION: 02.34.00)
=======
# MokoWaaS Plugin Overview (VERSION: 02.34.16)
>>>>>>> origin/dev
## Introduction ## Introduction
+1 -5
View File
@@ -10,11 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md PATH: /docs/update-server.md
<<<<<<< HEAD VERSION: 02.34.45
VERSION: 02.34.00
=======
VERSION: 02.34.16
>>>>>>> origin/dev
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
--> -->
+57 -27
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
Extension catalog for MokoWaaS Extension Manager. Extension catalog for MokoWaaS 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>
@@ -19,6 +19,16 @@
<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/MokoWaaS/raw/branch/dev/updates.xml</updateserver>
</extension> </extension>
<extension>
<name>MokoWaaSHQ</name>
<element>pkg_mokowaashq</element>
<type>package</type>
<description>Centralized control panel for managing all MokoWaaS client installations.</description>
<icon>icon-tachometer-alt</icon>
<category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokowaas-base</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaSHQ/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension> <extension>
<name>MokoOnyx</name> <name>MokoOnyx</name>
<element>mokoonyx</element> <element>mokoonyx</element>
@@ -30,14 +40,24 @@
<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>MokoJoomBackup</name>
<element>pkg_mokojoombackup</element>
<type>package</type>
<description>Automated backup system with Borg integration, scheduled tasks, and remote storage.</description>
<icon>icon-archive</icon>
<category>Tools</category>
<article>https://mokoconsulting.tech/support/products/mokojoombackup</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/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>
@@ -133,3 +133,4 @@ INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `rete
(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 @@
--
-- MokoWaaS component uninstall — drop all tables
--
DROP TABLE IF EXISTS `#__mokowaas_download_keys`;
DROP TABLE IF EXISTS `#__mokowaas_retention_policies`;
DROP TABLE IF EXISTS `#__mokowaas_data_requests`;
DROP TABLE IF EXISTS `#__mokowaas_consent_log`;
DROP TABLE IF EXISTS `#__mokowaas_waf_log`;
DROP TABLE IF EXISTS `#__mokowaas_ticket_automation`;
DROP TABLE IF EXISTS `#__mokowaas_ticket_canned`;
DROP TABLE IF EXISTS `#__mokowaas_ticket_replies`;
DROP TABLE IF EXISTS `#__mokowaas_tickets`;
DROP TABLE IF EXISTS `#__mokowaas_ticket_categories`;
@@ -0,0 +1,2 @@
-- Remove download_keys table (feature reverted — preflight handles key preservation)
DROP TABLE IF EXISTS `#__mokowaas_download_keys`;
@@ -0,0 +1,2 @@
-- RSA signing replaces key ring — drop table if it was created
DROP TABLE IF EXISTS `#__mokowaas_api_keys`;
@@ -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', 'mokowaas_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/mokowaas_monitor/mokowaas_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' => 'MokoWaaSHQ URL not configured in monitor plugin.']);
return;
}
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas');
$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/mokowaas_monitor/mokowaas_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-MokoWaaS-Signature: ' . base64_encode($signature);
$headers[] = 'X-MokoWaaS-Timestamp: ' . $timestamp;
}
}
}
$endpoint = $baseUrl . '/api/index.php/v1/mokowaashq/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
// ================================================================== // ==================================================================
@@ -714,6 +868,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;
} }
} }
@@ -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,
]; ];
} }
@@ -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).
* *
@@ -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): ?>
@@ -82,7 +90,9 @@ $statusBadge = [
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokowaas&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>
@@ -91,7 +101,9 @@ $statusBadge = [
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokowaas&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>
@@ -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',
@@ -20,7 +20,7 @@ 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/mokowaas/remote-login
* Body: {"token": "health_api_token", "user": "requesting_username", "origin": "MokoWaaSBase"} * Body: {"token": "health_api_token", "user": "requesting_username", "origin": "MokoWaaSHQ"}
* *
* 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.
@@ -0,0 +1,38 @@
/**
* MokoWaaS+ERP Customer Portal styles
* @since 02.34.16
*/
.mokowaas-portal h2,
.mokowaas-portal-orders h2,
.mokowaas-portal-invoices h2,
.mokowaas-portal-license h2 {
color: #1a2744;
font-weight: 700;
}
/* Signing page */
.mokowaas-sign-page {
max-width: 800px;
margin: 0 auto;
}
#signature-canvas {
border: 1px solid #dee2e6;
border-radius: 4px;
background: #fff;
}
/* Verification page */
.mokowaas-verify-page {
max-width: 900px;
margin: 0 auto;
}
/* Portal cards */
.mokowaas-portal .card {
transition: box-shadow 0.15s;
}
.mokowaas-portal .card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
@@ -0,0 +1,162 @@
/**
* MokoWaaS+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/mokowaas/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/mokowaas/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('.mokowaas-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('.mokowaas-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/mokowaas/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('.mokowaas-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('.mokowaas-sign-page').appendChild(msg);
});
});
}
});
+13 -1
View File
@@ -20,11 +20,23 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description> <description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoWaaS</namespace> <namespace path="src">Moko\Component\MokoWaaS</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> <administration>
<menu img="class:cogs">MokoWaaS</menu> <menu img="class:cogs">MokoWaaS</menu>
<submenu> <submenu>
@@ -0,0 +1,102 @@
<?php
namespace Moko\Component\MokoWaaS\Site\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
/**
* Portal Model — resolves logged-in user to ERP contact and loads their data.
*/
class PortalModel extends BaseDatabaseModel
{
/**
* Get the ERP contact ID for the current logged-in user (matched by email).
*/
public function getContactId(): int
{
$user = Factory::getUser();
if ($user->guest) { return 0; }
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('id')
->from($db->quoteName('#__contact_details'))
->where($db->quoteName('email_to') . ' = ' . $db->quote($user->email))
->where($db->quoteName('published') . ' = 1')
->setLimit(1)
);
return (int) $db->loadResult();
}
public function getDashboard(int $contactId): object
{
$db = $this->getDatabase();
$dash = new \stdClass();
// Open orders
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokowaas_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->where($db->quoteName('status') . ' NOT IN (' . $db->quote('delivered') . ',' . $db->quote('cancelled') . ')'));
$dash->open_orders = (int) $db->loadResult();
// Unpaid invoices
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->select('COALESCE(SUM(total - amount_paid), 0) AS total_due')->from($db->quoteName('#__mokowaas_erp_invoices'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->where($db->quoteName('status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('partial') . ',' . $db->quote('overdue') . ')'));
$inv = $db->loadObject();
$dash->unpaid_invoices = (int) $inv->{'COUNT(*)'};
$dash->total_due = (float) $inv->total_due;
// Open tickets
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokowaas_tickets'))->where($db->quoteName('created_by') . ' = ' . (int) Factory::getUser()->id)->where($db->quoteName('status') . ' NOT IN (' . $db->quote('closed') . ',' . $db->quote('resolved') . ')'));
$dash->open_tickets = (int) $db->loadResult();
// Pending signatures
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokowaas_erp_esign_signers'))->where($db->quoteName('email') . ' = ' . $db->quote(Factory::getUser()->email))->where($db->quoteName('status') . ' IN (' . $db->quote('pending') . ',' . $db->quote('viewed') . ')'));
$dash->pending_signatures = (int) $db->loadResult();
// Recent orders
$db->setQuery($db->getQuery(true)->select('id, ref, status, total, created')->from($db->quoteName('#__mokowaas_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, 5);
$dash->recent_orders = $db->loadObjectList() ?: [];
return $dash;
}
public function getOrders(int $contactId, int $limit = 25): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)->select('*')->from($db->quoteName('#__mokowaas_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, $limit);
return $db->loadObjectList() ?: [];
}
public function getOrder(int $contactId, int $id): ?object
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)->select('*')->from($db->quoteName('#__mokowaas_erp_orders'))->where($db->quoteName('id') . ' = ' . $id)->where($db->quoteName('contact_id') . ' = ' . $contactId));
$order = $db->loadObject();
if (!$order) { return null; }
$db->setQuery($db->getQuery(true)->select('oi.*, p.sku')->from($db->quoteName('#__mokowaas_erp_order_items', 'oi'))->join('LEFT', $db->quoteName('#__mokowaas_erp_products', 'p') . ' ON p.id = oi.product_id')->where($db->quoteName('oi.order_id') . ' = ' . $id)->order('oi.position ASC'));
$order->items = $db->loadObjectList() ?: [];
return $order;
}
public function getInvoices(int $contactId, int $limit = 25): array
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)->select('*, (total - amount_paid) AS balance_due')->from($db->quoteName('#__mokowaas_erp_invoices'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, $limit);
return $db->loadObjectList() ?: [];
}
public function getInvoice(int $contactId, int $id): ?object
{
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)->select('*, (total - amount_paid) AS balance_due')->from($db->quoteName('#__mokowaas_erp_invoices'))->where($db->quoteName('id') . ' = ' . $id)->where($db->quoteName('contact_id') . ' = ' . $contactId));
$inv = $db->loadObject();
if (!$inv) { return null; }
$db->setQuery($db->getQuery(true)->select('ii.*, p.sku')->from($db->quoteName('#__mokowaas_erp_invoice_items', 'ii'))->join('LEFT', $db->quoteName('#__mokowaas_erp_products', 'p') . ' ON p.id = ii.product_id')->where($db->quoteName('ii.invoice_id') . ' = ' . $id)->order('ii.position ASC'));
$inv->items = $db->loadObjectList() ?: [];
return $inv;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Invoice;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $invoice;
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
$model = $this->getModel('Portal');
$contactId = $model->getContactId();
$this->invoice = $contactId ? $model->getInvoice($contactId, Factory::getApplication()->getInput()->getInt('id', 0)) : null;
if (!$this->invoice) { throw new \Exception('Invoice not found', 404); }
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Invoices;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $items = [];
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
$model = $this->getModel('Portal');
$contactId = $model->getContactId();
$this->items = $contactId ? $model->getInvoices($contactId) : [];
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\License;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $licenseData;
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
// License data would come from plg_system_mokowaas_license cache
// For now, placeholder structure
$this->licenseData = (object) [
'valid' => true,
'package' => 'MokoWaaS+ERP',
'services' => ['base', 'erp'],
'expiry' => null,
'dlid' => '',
];
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Order;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $order;
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
$model = $this->getModel('Portal');
$contactId = $model->getContactId();
$this->order = $contactId ? $model->getOrder($contactId, Factory::getApplication()->getInput()->getInt('id', 0)) : null;
if (!$this->order) { throw new \Exception('Order not found', 404); }
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Orders;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $items = [];
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
$model = $this->getModel('Portal');
$contactId = $model->getContactId();
$this->items = $contactId ? $model->getOrders($contactId) : [];
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Portal;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $dashboard;
protected $contactId = 0;
public function display($tpl = null)
{
$user = Factory::getUser();
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
$model = $this->getModel();
$this->contactId = $model->getContactId();
$this->dashboard = $this->contactId ? $model->getDashboard($this->contactId) : null;
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,41 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Sign;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
/**
* Public signing page — token-based, no login required.
*/
class HtmlView extends BaseHtmlView
{
protected $signer;
protected $request;
public function display($tpl = null)
{
$token = Factory::getApplication()->getInput()->get('token', '', 'ALNUM');
if ($token && \strlen($token) === 128)
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('s.*, r.title AS request_title, r.description AS request_description, r.status AS request_status, r.require_selfie, r.require_id, r.require_consent')
->from($db->quoteName('#__mokowaas_erp_esign_signers', 's'))
->join('INNER', $db->quoteName('#__mokowaas_erp_esign_requests', 'r') . ' ON r.id = s.request_id')
->where($db->quoteName('s.token') . ' = ' . $db->quote($token))
);
$this->signer = $db->loadObject();
$this->request = $this->signer ? (object) ['title' => $this->signer->request_title, 'description' => $this->signer->request_description, 'status' => $this->signer->request_status] : null;
}
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
$wa->registerAndUseScript('com_mokowaas.signature-pad', 'com_mokowaas/signature-pad.js', [], ['defer' => true]);
parent::display($tpl);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\SignVerify;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $request;
public function display($tpl = null)
{
$hash = Factory::getApplication()->getInput()->get('hash', '', 'ALNUM');
if ($hash) {
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->select('id, ref, title, status, date_creation, date_signature')->from($db->quoteName('#__mokowaas_erp_esign_requests'))->where($db->quoteName('verification_hash') . ' = ' . $db->quote($hash)));
$this->request = $db->loadObject();
if ($this->request) {
$db->setQuery($db->getQuery(true)->select('role, email, firstname, lastname, status, date_signed, ip_address, geo_country, geo_city')->from($db->quoteName('#__mokowaas_erp_esign_signers'))->where($db->quoteName('request_id') . ' = ' . (int) $this->request->id)->order('position ASC'));
$this->request->signers = $db->loadObjectList() ?: [];
$db->setQuery($db->getQuery(true)->select('code, label, ip, created')->from($db->quoteName('#__mokowaas_erp_esign_events'))->where($db->quoteName('request_id') . ' = ' . (int) $this->request->id)->order('created ASC'));
$this->request->events = $db->loadObjectList() ?: [];
}
}
Factory::getApplication()->getDocument()->getWebAssetManager()
->registerAndUseStyle('com_mokowaas.portal', 'com_mokowaas/portal.css');
parent::display($tpl);
}
}
@@ -0,0 +1,39 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
$inv = $this->invoice;
?>
<div class="mokowaas-portal-invoice">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=invoices'); ?>" class="btn btn-sm btn-outline-secondary mb-3"><span class="icon-arrow-left"></span> My Invoices</a>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between">
<h3 class="mb-0 font-monospace"><?php echo $this->escape($inv->ref); ?></h3>
<span class="badge bg-<?php echo $inv->status === 'paid' ? 'success' : 'primary'; ?> fs-6"><?php echo ucfirst($inv->status); ?></span>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-4"><div class="text-muted small">Due Date</div><div class="fw-bold"><?php echo $this->escape($inv->due_date ?? 'On receipt'); ?></div></div>
<div class="col-md-4"><div class="text-muted small">Balance Due</div><div class="fs-4 fw-bold <?php echo (float) $inv->balance_due > 0 ? 'text-danger' : 'text-success'; ?>">$<?php echo number_format((float) $inv->balance_due, 2); ?></div></div>
<div class="col-md-4"><div class="text-muted small">Created</div><div><?php echo $this->escape($inv->created); ?></div></div>
</div>
<table class="table table-sm">
<thead class="table-light"><tr><th>Description</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
<tbody>
<?php foreach ($inv->items as $item) : ?>
<tr>
<td><?php echo $this->escape($item->description); ?></td>
<td class="text-end"><?php echo number_format((float) $item->quantity, 2); ?></td>
<td class="text-end">$<?php echo number_format((float) $item->unit_price, 2); ?></td>
<td class="text-end fw-bold">$<?php echo number_format((float) $item->line_total, 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-light">
<tr><td colspan="3" class="text-end">Subtotal</td><td class="text-end">$<?php echo number_format((float) $inv->subtotal, 2); ?></td></tr>
<tr><td colspan="3" class="text-end">Tax</td><td class="text-end">$<?php echo number_format((float) $inv->tax_total, 2); ?></td></tr>
<tr><td colspan="3" class="text-end fw-bold fs-5">Total</td><td class="text-end fw-bold fs-5">$<?php echo number_format((float) $inv->total, 2); ?></td></tr>
</tfoot>
</table>
</div>
</div>
</div>
@@ -0,0 +1,31 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
?>
<div class="mokowaas-portal-invoices">
<div class="d-flex justify-content-between mb-3">
<h2>My Invoices</h2>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
</div>
<div class="card shadow-sm"><div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th class="text-end">Total</th><th class="text-end">Paid</th><th class="text-end">Balance</th><th>Due</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($this->items as $inv) :
$isOverdue = $inv->due_date && $inv->due_date < date('Y-m-d') && \in_array($inv->status, ['sent', 'partial', 'overdue']);
?>
<tr class="<?php echo $isOverdue ? 'table-warning' : ''; ?>">
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=invoice&id=' . (int) $inv->id); ?>" class="font-monospace fw-bold"><?php echo $this->escape($inv->ref); ?></a></td>
<td><span class="badge bg-<?php echo $inv->status === 'paid' ? 'success' : ($isOverdue ? 'danger' : 'primary'); ?>"><?php echo ucfirst($inv->status); ?></span></td>
<td class="text-end">$<?php echo number_format((float) $inv->total, 2); ?></td>
<td class="text-end text-success">$<?php echo number_format((float) $inv->amount_paid, 2); ?></td>
<td class="text-end <?php echo (float) $inv->balance_due > 0 ? 'text-danger fw-bold' : ''; ?>">$<?php echo number_format((float) $inv->balance_due, 2); ?></td>
<td class="small <?php echo $isOverdue ? 'text-danger fw-bold' : 'text-muted'; ?>"><?php echo $this->escape($inv->due_date ?? '—'); ?></td>
<td class="small text-muted"><?php echo $this->escape($inv->created); ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($this->items)) : ?><tr><td colspan="7" class="text-center text-muted py-4">No invoices found</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
@@ -0,0 +1,58 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$lic = $this->licenseData;
?>
<div class="mokowaas-portal-license">
<div class="d-flex justify-content-between mb-3">
<h2>License & Subscription</h2>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header"><h5 class="mb-0">Current License</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="text-muted small">Package</div>
<div class="fs-5 fw-bold"><?php echo $this->escape($lic->package); ?></div>
</div>
<div class="col-md-4">
<div class="text-muted small">Status</div>
<div><span class="badge bg-<?php echo $lic->valid ? 'success' : 'danger'; ?> fs-6"><?php echo $lic->valid ? 'Active' : 'Invalid'; ?></span></div>
</div>
<div class="col-md-4">
<div class="text-muted small">Expires</div>
<div class="fw-bold"><?php echo $lic->expiry ? $this->escape($lic->expiry) : 'No expiry'; ?></div>
</div>
</div>
<?php if (!empty($lic->services)) : ?>
<hr>
<div class="text-muted small mb-2">Active Services</div>
<div class="d-flex gap-2 flex-wrap">
<?php foreach ($lic->services as $svc) : ?>
<span class="badge bg-primary"><?php echo strtoupper($this->escape($svc)); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header"><h5 class="mb-0">Update License Key</h5></div>
<div class="card-body">
<form method="post" action="<?php echo Route::_('index.php?option=com_mokowaas&task=saveLicense'); ?>">
<div class="mb-3">
<label class="form-label">Download Key (DLID)</label>
<input type="text" name="dlid" class="form-control font-monospace" placeholder="Enter your license key" value="<?php echo $this->escape($lic->dlid); ?>">
<div class="form-text">Enter or update your license key to activate features.</div>
</div>
<input type="hidden" name="<?php echo Session::getFormToken(); ?>" value="1">
<button type="submit" class="btn btn-primary"><span class="icon-key"></span> Save License Key</button>
</form>
</div>
</div>
</div>
@@ -0,0 +1,35 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
$o = $this->order;
?>
<div class="mokowaas-portal-order">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=orders'); ?>" class="btn btn-sm btn-outline-secondary mb-3"><span class="icon-arrow-left"></span> My Orders</a>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between">
<h3 class="mb-0 font-monospace"><?php echo $this->escape($o->ref); ?></h3>
<span class="badge bg-primary fs-6"><?php echo ucfirst($o->status); ?></span>
</div>
<div class="card-body">
<table class="table table-sm">
<thead class="table-light"><tr><th>SKU</th><th>Description</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
<tbody>
<?php foreach ($o->items as $item) : ?>
<tr>
<td class="font-monospace small"><?php echo $this->escape($item->sku ?? ''); ?></td>
<td><?php echo $this->escape($item->description); ?></td>
<td class="text-end"><?php echo number_format((float) $item->quantity, 2); ?></td>
<td class="text-end">$<?php echo number_format((float) $item->unit_price, 2); ?></td>
<td class="text-end fw-bold">$<?php echo number_format((float) $item->line_total, 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-light">
<tr><td colspan="4" class="text-end">Subtotal</td><td class="text-end">$<?php echo number_format((float) $o->subtotal, 2); ?></td></tr>
<tr><td colspan="4" class="text-end">Tax</td><td class="text-end">$<?php echo number_format((float) $o->tax_total, 2); ?></td></tr>
<tr><td colspan="4" class="text-end fw-bold fs-5">Total</td><td class="text-end fw-bold fs-5">$<?php echo number_format((float) $o->total, 2); ?></td></tr>
</tfoot>
</table>
</div>
</div>
</div>
@@ -0,0 +1,27 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
?>
<div class="mokowaas-portal-orders">
<div class="d-flex justify-content-between mb-3">
<h2>My Orders</h2>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
</div>
<div class="card shadow-sm"><div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th>Payment</th><th class="text-end">Total</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($this->items as $order) : ?>
<tr>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=order&id=' . (int) $order->id); ?>" class="font-monospace fw-bold"><?php echo $this->escape($order->ref); ?></a></td>
<td><span class="badge bg-primary"><?php echo ucfirst($order->status); ?></span></td>
<td><span class="badge bg-<?php echo $order->payment_status === 'paid' ? 'success' : 'warning'; ?>"><?php echo ucfirst($order->payment_status); ?></span></td>
<td class="text-end fw-bold">$<?php echo number_format((float) $order->total, 2); ?></td>
<td class="small text-muted"><?php echo $this->escape($order->created); ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($this->items)) : ?><tr><td colspan="5" class="text-center text-muted py-4">No orders found</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
@@ -0,0 +1,96 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
$dash = $this->dashboard;
$user = \Joomla\CMS\Factory::getUser();
?>
<div class="mokowaas-portal">
<h2 class="mb-4">Welcome, <?php echo $this->escape($user->name); ?></h2>
<?php if (!$this->contactId) : ?>
<div class="alert alert-warning">Your account is not linked to an ERP contact. Please contact support.</div>
<?php else : ?>
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm">
<div class="card-body">
<div class="fs-3 fw-bold"><?php echo (int) $dash->open_orders; ?></div>
<div class="small text-muted">Open Orders</div>
</div>
<div class="card-footer bg-transparent border-0">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=orders'); ?>" class="btn btn-sm btn-outline-primary w-100">View</a>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm <?php echo (int) $dash->unpaid_invoices > 0 ? 'border-warning' : ''; ?>">
<div class="card-body">
<div class="fs-3 fw-bold <?php echo (float) $dash->total_due > 0 ? 'text-warning' : ''; ?>"><?php echo (int) $dash->unpaid_invoices; ?></div>
<div class="small text-muted">Unpaid Invoices</div>
<?php if ((float) $dash->total_due > 0) : ?><div class="small fw-bold text-warning">$<?php echo number_format($dash->total_due, 2); ?> due</div><?php endif; ?>
</div>
<div class="card-footer bg-transparent border-0">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=invoices'); ?>" class="btn btn-sm btn-outline-warning w-100">View</a>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm">
<div class="card-body">
<div class="fs-3 fw-bold"><?php echo (int) $dash->open_tickets; ?></div>
<div class="small text-muted">Open Tickets</div>
</div>
<div class="card-footer bg-transparent border-0">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=tickets'); ?>" class="btn btn-sm btn-outline-primary w-100">View</a>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm <?php echo (int) $dash->pending_signatures > 0 ? 'border-info' : ''; ?>">
<div class="card-body">
<div class="fs-3 fw-bold"><?php echo (int) $dash->pending_signatures; ?></div>
<div class="small text-muted">Pending Signatures</div>
</div>
<div class="card-footer bg-transparent border-0">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=sign'); ?>" class="btn btn-sm btn-outline-info w-100">Sign</a>
</div>
</div>
</div>
</div>
<!-- Recent Orders -->
<?php if (!empty($dash->recent_orders)) : ?>
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between">
<h5 class="mb-0">Recent Orders</h5>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=orders'); ?>" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th class="text-end">Total</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($dash->recent_orders as $order) : ?>
<tr>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=order&id=' . (int) $order->id); ?>" class="font-monospace"><?php echo $this->escape($order->ref); ?></a></td>
<td><span class="badge bg-primary"><?php echo ucfirst($order->status); ?></span></td>
<td class="text-end fw-bold">$<?php echo number_format((float) $order->total, 2); ?></td>
<td class="small text-muted"><?php echo $this->escape($order->created); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Quick Links -->
<div class="row g-3">
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=license'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-key"></span> License & Subscription</a></div>
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=tickets&layout=submit'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-life-ring"></span> Submit Ticket</a></div>
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=invoices'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-file-invoice"></span> My Invoices</a></div>
</div>
<?php endif; ?>
</div>
@@ -0,0 +1,75 @@
<?php
defined('_JEXEC') or die;
$signer = $this->signer;
$request = $this->request;
$token = \Joomla\CMS\Factory::getApplication()->getInput()->get('token', '', 'ALNUM');
?>
<div class="mokowaas-sign-page">
<?php if (!$signer) : ?>
<div class="alert alert-danger">Invalid or expired signing link.</div>
<?php elseif ($signer->status === 'signed') : ?>
<div class="alert alert-success"><strong>Already signed.</strong> You have already signed this document on <?php echo $this->escape($signer->date_signed); ?>.</div>
<?php elseif (!\in_array($request->status, ['pending', 'inprogress'])) : ?>
<div class="alert alert-warning">This signing request is no longer active (status: <?php echo $this->escape($request->status); ?>).</div>
<?php else : ?>
<div class="card shadow-sm mb-4">
<div class="card-header"><h3 class="mb-0"><?php echo $this->escape($request->title); ?></h3></div>
<div class="card-body">
<?php if ($request->description) : ?>
<div class="border rounded p-3 mb-3 bg-light" style="max-height:400px;overflow-y:auto;">
<?php echo nl2br($this->escape($request->description)); ?>
</div>
<?php endif; ?>
<form id="signing-form" data-token="<?php echo $this->escape($token); ?>">
<!-- Consent -->
<?php if ($signer->require_consent && !$signer->consent_accepted) : ?>
<div class="alert alert-info">
<div class="form-check">
<input type="checkbox" id="consent-checkbox" class="form-check-input" required>
<label for="consent-checkbox" class="form-check-label">
I agree to use electronic signatures and understand this is legally binding.
</label>
</div>
</div>
<?php endif; ?>
<!-- Signature Pad -->
<div class="mb-3">
<label class="form-label fw-bold">Your Signature</label>
<div class="border rounded p-2 bg-white">
<canvas id="signature-canvas" width="600" height="200" style="width:100%;height:200px;cursor:crosshair;touch-action:none;"></canvas>
</div>
<button type="button" id="clear-signature" class="btn btn-sm btn-outline-secondary mt-1">Clear</button>
</div>
<!-- Optional Selfie -->
<?php if ($signer->require_selfie) : ?>
<div class="mb-3">
<label class="form-label fw-bold">Selfie Verification</label>
<div><button type="button" id="btn-selfie" class="btn btn-outline-info btn-sm"><span class="icon-camera"></span> Take Selfie</button></div>
<canvas id="selfie-preview" class="mt-2 d-none border rounded" width="320" height="240"></canvas>
</div>
<?php endif; ?>
<!-- Optional ID Photo -->
<?php if ($signer->require_id) : ?>
<div class="mb-3">
<label class="form-label fw-bold">ID Verification</label>
<div><button type="button" id="btn-id-photo" class="btn btn-outline-info btn-sm"><span class="icon-id-card"></span> Take ID Photo</button></div>
<canvas id="id-preview" class="mt-2 d-none border rounded" width="320" height="240"></canvas>
</div>
<?php endif; ?>
<div class="d-flex gap-2 mt-4">
<button type="submit" id="btn-sign" class="btn btn-success btn-lg flex-grow-1"><span class="icon-pen-nib"></span> Sign Document</button>
<button type="button" id="btn-decline" class="btn btn-outline-danger">Decline</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
</div>
@@ -0,0 +1,64 @@
<?php
defined('_JEXEC') or die;
$req = $this->request;
?>
<div class="mokowaas-verify-page">
<?php if (!$req) : ?>
<div class="alert alert-danger">Verification not found. Check the verification link.</div>
<?php else : ?>
<div class="card shadow-sm mb-3">
<div class="card-header text-center">
<h3 class="mb-0">Certificate of Verification</h3>
<div class="small text-muted">Electronic Signature Verification</div>
</div>
<div class="card-body">
<div class="text-center mb-4">
<?php if ($req->status === 'completed') : ?>
<div class="alert alert-success fs-5"><span class="icon-check-circle"></span> This document has been fully signed and is legally valid.</div>
<?php else : ?>
<div class="alert alert-warning">Status: <?php echo ucfirst($req->status); ?> — this document has not been fully signed.</div>
<?php endif; ?>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4"><div class="text-muted small">Reference</div><div class="font-monospace fw-bold"><?php echo $this->escape($req->ref); ?></div></div>
<div class="col-md-4"><div class="text-muted small">Title</div><div><?php echo $this->escape($req->title); ?></div></div>
<div class="col-md-4"><div class="text-muted small">Completed</div><div><?php echo $this->escape($req->date_signature ?? 'Pending'); ?></div></div>
</div>
<h5>Signers</h5>
<table class="table table-sm">
<thead class="table-light"><tr><th>Name</th><th>Email</th><th>Status</th><th>Signed</th><th>Location</th></tr></thead>
<tbody>
<?php foreach ($req->signers as $s) :
$name = trim(($s->firstname ?? '') . ' ' . ($s->lastname ?? '')) ?: '—';
$loc = implode(', ', array_filter([$s->geo_city ?? '', $s->geo_country ?? ''])) ?: '—';
?>
<tr>
<td><?php echo $this->escape($name); ?></td>
<td class="small"><?php echo $this->escape($s->email); ?></td>
<td><span class="badge bg-<?php echo $s->status === 'signed' ? 'success' : ($s->status === 'declined' ? 'danger' : 'warning'); ?>"><?php echo ucfirst($s->status); ?></span></td>
<td class="small"><?php echo $this->escape($s->date_signed ?? '—'); ?></td>
<td class="small"><?php echo $this->escape($loc); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h5 class="mt-4">Audit Trail</h5>
<table class="table table-sm">
<thead class="table-light"><tr><th>Time</th><th>Event</th><th>Description</th><th>IP</th></tr></thead>
<tbody>
<?php foreach ($req->events as $ev) : ?>
<tr>
<td class="small text-muted"><?php echo $this->escape($ev->created); ?></td>
<td><span class="badge bg-info"><?php echo $this->escape($ev->code); ?></span></td>
<td class="small"><?php echo $this->escape($ev->label); ?></td>
<td class="font-monospace small"><?php echo $this->escape($ev->ip ?? '—'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description> <description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace> <namespace path="src">Moko\Module\MokoWaaSCache</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>MOD_MOKOWAAS_CATEGORIES_DESC</description> <description>MOD_MOKOWAAS_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCategories</namespace> <namespace path="src">Moko\Module\MokoWaaSCategories</namespace>
@@ -7,11 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml
<description>MOD_MOKOWAAS_CPANEL_DESC</description> <description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace> <namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
@@ -47,6 +47,29 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
$data['currentIp'] = $helper->getCurrentIp(); $data['currentIp'] = $helper->getCurrentIp();
$data['ssl'] = $helper->getSslStatus(); $data['ssl'] = $helper->getSslStatus();
// Support PIN derived from health token
$data['supportPin'] = '';
try
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
$coreParams = json_decode((string) $db->loadResult());
$token = $coreParams->health_api_token ?? '';
if (!empty($token))
{
$data['supportPin'] = 'MOKO-' . strtoupper(substr($token, 0, 4) . '-' . substr($token, 4, 4));
}
}
catch (\Throwable $e) {}
return $data; return $data;
} }
} }
@@ -66,6 +66,9 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span> <span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
<strong>MokoWaaS</strong> <strong>MokoWaaS</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span> <span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
<?php if (!empty($supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;" title="Support PIN"><?php echo htmlspecialchars($supportPin); ?></span>
<?php endif; ?>
<?php if (!empty($siteInfo->debug)): ?> <?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span> <span class="badge bg-warning text-dark">Debug</span>
<?php endif; ?> <?php endif; ?>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>MokoWaaS admin sidebar menu — renders a dedicated MokoWaaS section in the admin menu before Joomla's default menu.</description> <description>MokoWaaS admin sidebar menu — renders a dedicated MokoWaaS section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoWaaSMenu</namespace> <namespace path="src">Moko\Module\MokoWaaSMenu</namespace>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas * REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.16 * VERSION: 02.34.45
* PATH: /src/Extension/MokoWaaS.php * PATH: /src/Extension/MokoWaaS.php
* NOTE: Core system plugin for MokoWaaS admin tools suite * NOTE: Core system plugin for MokoWaaS admin tools suite
*/ */
@@ -47,13 +47,6 @@ use Psr\Container\ContainerInterface;
*/ */
class MokoWaaS extends CMSPlugin implements BootableExtensionInterface class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{ {
/**
* Obfuscated Grafana URL (XOR + base64).
*
* @var string
* @since 02.01.26
*/
private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat';
/** /**
* Obfuscated master usernames (XOR 0x5A + base64). * Obfuscated master usernames (XOR 0x5A + base64).
@@ -75,8 +68,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
* @var string * @var string
* @since 02.01.36 * @since 02.01.36
*/ */
private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m';
/** /**
* Get the plugin version from the manifest XML. * Get the plugin version from the manifest XML.
* *
@@ -172,7 +163,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{ {
$this->handleOneTimeLogin(); $this->handleOneTimeLogin();
$this->checkSetupRequired(); $this->checkSetupRequired();
$this->preserveDownloadKeys();
} }
} }
@@ -240,8 +230,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
} }
} }
// Grafana auto-provisioning
$this->handleGrafanaProvisioning($params, $app);
// Clear setup-required flag on save (new client setup complete) // Clear setup-required flag on save (new client setup complete)
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag'; $flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
@@ -1871,127 +1859,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/** /**
* Send heartbeat to the MokoWaaS monitoring receiver.
*
* Registers this site's primary domain with the Grafana provisioning system.
* The receiver writes a datasource YAML file and restarts Grafana.
* Alias domains are not registered to avoid duplicate datasource UIDs.
*
* @param \Joomla\Registry\Registry $params Plugin params
* @param \Joomla\CMS\Application\CMSApplication $app Application
*
* @return void
*
* @since 02.01.36
*/
protected function handleGrafanaProvisioning($params, $app)
{
$healthToken = $params->get('health_api_token', '');
if (empty($healthToken))
{
return;
}
$siteUrl = rtrim(Uri::root(), '/');
$siteName = Factory::getConfig()->get('sitename', 'Joomla');
// Register primary domain
$this->sendHeartbeat($siteUrl, $siteName, $healthToken, $app);
// Register alias domains (subform format)
$aliases = $params->get('site_aliases', '');
if (!empty($aliases))
{
if (is_string($aliases))
{
$aliases = json_decode($aliases);
}
if (is_object($aliases))
{
$aliases = (array) $aliases;
}
if (is_array($aliases))
{
foreach ($aliases as $alias)
{
$alias = (object) $alias;
if (!empty($alias->domain))
{
$domain = rtrim(trim($alias->domain), '/');
$aliasUrl = 'https://' . preg_replace('#^https?://#i', '', $domain);
$this->sendHeartbeat($aliasUrl, $siteName, $healthToken, $app);
}
}
}
}
}
/**
* Send a single heartbeat registration to the receiver.
*
* @param string $siteUrl Site URL to register
* @param string $siteName Display name for Grafana
* @param string $healthToken Health API bearer token
* @param object $app Application for messages
*
* @return void
*
* @since 02.01.39
*/
protected function sendHeartbeat($siteUrl, $siteName, $healthToken, $app)
{
$payload = json_encode([
'site_url' => $siteUrl,
'site_name' => $siteName,
'health_token' => $healthToken,
'action' => 'register',
], JSON_UNESCAPED_SLASHES);
$ch = curl_init(self::HEARTBEAT_URL . '/register');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-MokoWaaS-Key: ' . self::HEARTBEAT_KEY,
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
$body = json_decode($response, true);
if ($error)
{
$app->enqueueMessage('Grafana heartbeat failed (' . $siteUrl . '): ' . $error, 'warning');
Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas');
}
elseif ($code === 200)
{
$status = $body['status'] ?? 'ok';
$app->enqueueMessage(
'Grafana heartbeat: ' . $siteUrl . ' ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')',
'message'
);
}
else
{
$msg = sprintf('Grafana heartbeat failed (%s): HTTP %d — %s',
$siteUrl, $code, $body['error'] ?? $body['message'] ?? 'Unknown');
$app->enqueueMessage($msg, 'warning');
Log::add($msg, Log::WARNING, 'mokowaas');
}
}
// HTTPS / Session / License (called from onAfterInitialise) // HTTPS / Session / License (called from onAfterInitialise)
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@@ -2132,7 +1999,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/** /**
* Handle one-time login tokens from MokoWaaSBase remote login. * Handle one-time login tokens from MokoWaaSHQ remote login.
* *
* Checks for ?mokowaas_otl=TOKEN in the admin URL, validates the * Checks for ?mokowaas_otl=TOKEN in the admin URL, validates the
* token against the stored OTL file, auto-logs in the master user, * token against the stored OTL file, auto-logs in the master user,
@@ -2237,91 +2104,4 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
* *
* @since 02.34.12 * @since 02.34.12
*/ */
protected function preserveDownloadKeys(): void
{
try
{
$db = Factory::getDbo();
// Load current extra_query values for all update sites
$query = $db->getQuery(true)
->select([
$db->quoteName('update_site_id'),
$db->quoteName('extra_query'),
$db->quoteName('location'),
])
->from($db->quoteName('#__update_sites'));
$db->setQuery($query);
$sites = $db->loadObjectList('update_site_id') ?: [];
$backupFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_dlkeys.json';
$backup = [];
if (file_exists($backupFile))
{
$backup = json_decode(file_get_contents($backupFile), true) ?: [];
}
$restored = 0;
$updated = false;
foreach ($sites as $id => $site)
{
$currentKey = trim((string) $site->extra_query);
$backupKey = $backup[$id] ?? '';
if ($currentKey !== '')
{
// Site has a key — update backup if changed
if ($currentKey !== $backupKey)
{
$backup[$id] = $currentKey;
$updated = true;
}
}
elseif ($backupKey !== '')
{
// Key was wiped — restore from backup
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($backupKey))
->where($db->quoteName('update_site_id') . ' = ' . (int) $id)
)->execute();
$restored++;
}
}
// Clean up backup entries for update sites that no longer exist
$currentIds = array_keys($sites);
foreach (array_keys($backup) as $backupId)
{
if (!isset($sites[$backupId]))
{
unset($backup[$backupId]);
$updated = true;
}
}
if ($updated || $restored > 0)
{
file_put_contents($backupFile, json_encode($backup, JSON_PRETTY_PRINT));
}
if ($restored > 0)
{
Log::add(
sprintf('MokoWaaS: restored %d download key(s) that were cleared by Joomla.', $restored),
Log::INFO,
'mokowaas'
);
}
}
catch (\Throwable $e)
{
// Non-critical — don't break the site over key backup
}
}
} }
@@ -8,11 +8,7 @@
* FILE INFORMATION * FILE INFORMATION
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
<<<<<<< HEAD:src/packages/plg_system_mokowaas/Field/CopyableTokenField.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.16
>>>>>>> origin/dev:source/packages/plg_system_mokowaas/Field/CopyableTokenField.php
* PATH: /src/Field/CopyableTokenField.php * PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button * BRIEF: Read-only token field with a copy-to-clipboard button
*/ */
@@ -22,6 +18,7 @@ namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField; use Joomla\CMS\Form\FormField;
use Joomla\CMS\Session\Session;
/** /**
* Renders a read-only text input with a "Copy" button, similar to * Renders a read-only text input with a "Copy" button, similar to
@@ -43,8 +40,9 @@ class CopyableTokenField extends FormField
return '<div class="alert alert-warning mb-0 py-2">Token will be generated automatically on first save.</div>'; return '<div class="alert alert-warning mb-0 py-2">Token will be generated automatically on first save.</div>';
} }
// Derive a human-readable support PIN from the token $pin = strtoupper(substr($this->value, 0, 4) . '-' . substr($this->value, 4, 4));
$pin = strtoupper(substr($this->value, 0, 4) . '-' . substr($this->value, 4, 4)); $token = Session::getFormToken();
$ajaxUrl = 'index.php?option=com_mokowaas&task=display.sendHeartbeat&format=json';
return <<<HTML return <<<HTML
<div class="input-group mb-2"> <div class="input-group mb-2">
@@ -64,6 +62,31 @@ class CopyableTokenField extends FormField
inp.select(); document.execCommand('copy'); inp.select(); document.execCommand('copy');
} }
"><span class="icon-copy" aria-hidden="true"></span> Copy</button> "><span class="icon-copy" aria-hidden="true"></span> Copy</button>
<button type="button" class="btn btn-outline-primary" id="mokowaas-send-heartbeat" onclick="
var btn = this;
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class=&quot;icon-spinner icon-spin&quot; aria-hidden=&quot;true&quot;></span> Sending...';
var fd = new FormData();
fd.append('{$token}', '1');
fetch('{$ajaxUrl}', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if(d.success){
btn.innerHTML='<span class=&quot;icon-check&quot; aria-hidden=&quot;true&quot;></span> Sent';
btn.classList.replace('btn-outline-primary','btn-success');
} else {
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Failed';
btn.classList.replace('btn-outline-primary','btn-danger');
}
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
})
.catch(function(){
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Error';
btn.classList.replace('btn-outline-primary','btn-danger');
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
});
"><span class="icon-heart" aria-hidden="true"></span> Send Heartbeat</button>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="badge bg-dark" style="font-family:monospace;font-size:1rem;letter-spacing:0.1em;">MOKO-{$pin}</span> <span class="badge bg-dark" style="font-family:monospace;font-size:1rem;letter-spacing:0.1em;">MOKO-{$pin}</span>
@@ -16,11 +16,11 @@ PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates
; ===== Core fieldset ===== ; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core" PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration." PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and MokoWaaSHQ integration."
; ===== Diagnostics ===== ; ===== Diagnostics =====
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token" PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter." PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your health monitoring configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
; ===== Site Aliases fieldset ===== ; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases" PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -28,7 +28,7 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mir
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain" PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix." PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases" PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource." PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own MokoWaaSHQ monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain" PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix." PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline" PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline"
@@ -16,11 +16,11 @@ PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates
; ===== Core fieldset ===== ; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core" PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration." PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and MokoWaaSHQ integration."
; ===== Diagnostics ===== ; ===== Diagnostics =====
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token" PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter." PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your health monitoring configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
; ===== Site Aliases fieldset ===== ; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases" PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -28,7 +28,7 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mir
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain" PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix." PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases" PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource." PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own MokoWaaSHQ monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain" PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix." PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_DESC="The alias domain name (e.g. www.example.com). Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline" PLG_SYSTEM_MOKOWAAS_ALIAS_OFFLINE_LABEL="Offline"
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license> <license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description> <description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace> <namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
+1 -58
View File
@@ -22,11 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas * REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD:src/packages/plg_system_mokowaas/script.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.16
>>>>>>> origin/dev:source/packages/plg_system_mokowaas/script.php
* PATH: /src/script.php * PATH: /src/script.php
* BRIEF: Installation script for MokoWaaS plugin * BRIEF: Installation script for MokoWaaS plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment * NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -729,59 +725,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
); );
$db->execute(); $db->execute();
} }
// Heartbeat receiver — register with Grafana provisioning
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$siteName = Factory::getConfig()->get('sitename', 'Joomla');
$token = $params->get('health_api_token', '');
$payload = json_encode([
'site_url' => $siteUrl,
'site_name' => $siteName,
'health_token' => $token,
'action' => 'register',
], JSON_UNESCAPED_SLASHES);
$ch = curl_init('https://bench.mokoconsulting.tech/api/waas-heartbeat/register');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
$app = Factory::getApplication();
$body = json_decode($response, true);
if ($error)
{
$app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning');
Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas');
}
elseif ($code === 200)
{
$status = $body['status'] ?? 'ok';
$app->enqueueMessage(
'Grafana heartbeat: ' . $status . ' (' . ($body['ds_uid'] ?? '') . ')',
'message'
);
}
else
{
$msg = sprintf('Grafana heartbeat failed: HTTP %d — %s',
$code, $body['error'] ?? $body['message'] ?? 'Unknown');
$app->enqueueMessage($msg, 'warning');
Log::add($msg, Log::WARNING, 'mokowaas');
}
} }
private function registerActionLogExtension() private function registerActionLogExtension()
@@ -22,11 +22,7 @@
* DEFGROUP: Joomla.Plugin * DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas * REPO: https://github.com/mokoconsulting-tech/mokowaas
<<<<<<< HEAD:src/packages/plg_system_mokowaas/services/provider.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.16
>>>>>>> origin/dev:source/packages/plg_system_mokowaas/services/provider.php
* PATH: /src/services/provider.php * PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x * BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container * NOTE: Registers the plugin with Joomla's DI container
@@ -8,11 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description> <description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace> <namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
@@ -8,11 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description> <description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace> <namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
@@ -3,11 +3,11 @@
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor"
PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Sends heartbeat data to a MokoWaaSBase control panel for centralized site monitoring." PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Sends heartbeat data to a MokoWaaSHQ control panel for centralized site monitoring."
PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC="Monitoring" PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC="Monitoring"
PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC_DESC="Configure heartbeat reporting to MokoWaaSBase." PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC_DESC="Configure heartbeat reporting to MokoWaaSHQ."
PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_LABEL="Send Heartbeat" PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_LABEL="Send Heartbeat"
PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_DESC="Send heartbeat data to MokoWaaSBase when plugin settings are saved." PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_DESC="Send heartbeat data to MokoWaaSHQ when plugin settings are saved."
PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_LABEL="MokoWaaSBase URL" PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_LABEL="MokoWaaSHQ URL"
PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_DESC="URL of the MokoWaaSBase control panel (e.g. https://mokoconsulting.tech). The heartbeat is sent to /api/index.php/v1/mokowaasbase/heartbeat on this host." PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_DESC="URL of the MokoWaaSHQ control panel (e.g. https://mokoconsulting.tech). The heartbeat is sent to /api/index.php/v1/mokowaashq/heartbeat on this host."
@@ -1,3 +1,3 @@
; MokoWaaS Health Monitor Plugin - System strings ; MokoWaaS Health Monitor Plugin - System strings
PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor"
PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, Grafana heartbeat integration, and diagnostics." PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, MokoWaaSHQ heartbeat integration, and diagnostics."
@@ -8,11 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description> <description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace> <namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
@@ -41,13 +37,13 @@
<option value="0">JNO</option> <option value="0">JNO</option>
</field> </field>
<field name="base_url" type="url" <field name="base_url" type="hidden"
label="PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_LABEL" default="https://waas.dev.mokoconsulting.tech"
description="PLG_SYSTEM_MOKOWAAS_MONITOR_BASE_URL_DESC" filter="url" />
default="https://mokoconsulting.tech"
filter="url" <field name="signing_key" type="hidden"
hint="https://mokoconsulting.tech" default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUUMvcnVrWE0zZHB0aDg2DQpGSkRXTjM0ZjQ2cUtJem1SMmFtTWUyZ2dWbWxsWnFyMHJkRFk4OTdtQ05FRkk4Q0NwNGR5amkwOU5ETnAvalFxDQovL2JGdUFNOUFTZU5oQTlmRlpwSG5UMGkzY3N4V3RSS2NnMnRkR0wzUXhNRFVBeFJYQ1RSQXVPSWZybGp6Ky85DQpWZ0ZtWHU3M1VSaU9XY1lLeFErejFoZkRGK2ZxRTRlYW9QcUlsY2J5dmtKd2lkSkRWUEEwc2RtbVlUTFg2Q29xDQpQalVDRENlbkZoUXNteVMzM29KSXArK0c3ZzU5NmRYelZIczRQSjIwNnc0Z3JlckRRZk5GVytzZndHSnl3NjBrDQpUQTVmUzF2Wit4NEt2UUh6V1ErYS9xRS9sSGxFVzdOTWVJWExNWGczSDd1eXBabXlVU2t3S0k0djFQQXRGWmtkDQpBaVpPZWZpVkFnTUJBQUVDZ2dFQVI4VGJyVDR0NWJ5MDhIQW0wcTR3WVF4REhEbVlJbzNXdDZ5MURmYU11OVMzDQpDYW5TMm9oazJzaE9TcGhhU2hFajI3WjBKY2hYdjhYWURvbU1BZmVsN3I5eDZjQ2FhTVdUNEdCMU5Zckp1NDhBDQprV2NteTkwWitPNTZQZkZJeTJXdXV6dFRxaFdZb0ZDSTBOZlU2bGw5SzhpSFl6VWx1MzZSSklweWx5OXFPKyt4DQpmTUZYcUovSkk0bVp6NW0raDBnbFMvN21VZ0EvUTRjbVJnRHJ3dkc3bEpBRjhWSDBEdW1uRWJkWkZvSi9XbU9JDQpSTi9lemhqczYrbU9hTnUwQWRsclpLU3QwRWZVYjl3QTFLQm5JMVVDU2w0Y1lidXVpL29jOWo1aGl6RGJvRWRyDQpJL1U5Y2FYUmZvb0pMNlUwOXN1VTdyTlFLbFRhMXM4NVhvY3htT0JMK1FLQmdRRHg5QzB5MjQ5SG1paXJ2WExIDQpBUXdUTjRyMjdhUTZMMFc2SHdDNHdzMUhleDRpeWRXT1lIcWdBSnY4VHZyeVpHOW1SaFh1U1ROTjYxV1UvTWFNDQphQVYwVjJ4Y0RrdDNFUnhNak1XRmhXUTh0cjN2RUtqWjFnOVJXOGhiTE9VYXVCcmJhMlI4RWNZYXFLZXlxR3N4DQpCa0VLZlRIUzNmUysraXNLZ2EzUU1mcjB6d0tCZ1FES3o2SGVKZ0tKRTVMM1ppbkhxaUFyVm5SZ2pYcFZrMWpvDQp6VXh5eTkwNEhmNGlmVXNIZklpdzVpN0VNR0U0RE5ob2MvZUJxcW1oM1N2ejJMUDNzOHUrL0hVZFllVzJIV1hhDQpKZlpMRE5BM0U3WDNkSVJ6MFg5UTh2OHcxaFpQeUxYOUlYeUVyUTNGZHFVdyt1Tko1VFZJell0RHppNnRKTjkvDQpGZGlxS0Q2ZFd3S0JnUURnQnE5bS9LWmdyTnRsa1FkYVBaejVtaDhBWGE4RzlNaEIrZnpJRmc3T1ZhL2tsQzg1DQpJaG5JVm1nWHFPVndWQkJWaVNVN09lbllCc042TE1hR01MYUVMNEkwaGtQWG5pOHVyZFVodVEzRHJZeVZjejUwDQpYR0JZZTN3Njk0bTJRS3NWYVExa1YyeXZPR1AxNXoxQTZrS0V2TURLTnhzclRTVlhHQlZneFRaUlB3S0JnUURBDQp1RFVVcUFIWXlDVHJ1c1VRMm5UZk9iUTAyN3ZYL2NDSzJDdEJHc0FJUjFmcTVpeVozSmozb0lQb0lpRC81aFR1DQpqT1F3N3o5cWRJVURublRGZUxDdnQ2NkNVVGk3cVl2VGxDZEtnYzZKeDgwdWJDWkErRjZIU2FGOWdyS0k5aTBaDQpjT3ltRnR2elBCOFZRQk1qY1E4Rk0yeVc3aUlrbmRsVEppdFE1aFU1NlFLQmdEZ1JIOXBEcGZwWlZ2V2g2MldGDQp5OGZzWUo1ODhzQmRMUlpTYTRuNi9XbjdUcUp1bWg2aWpFcDVyZFdnQkVtaDlJSk9jRUlhZ05mK0s5MXdoaThvDQpTeW01ajJpL1pjVVFYNFJSTDNxQ1RZZWVQVnZ3RHc3aWNLWVowTGQ2S1pFMmdEaDRPbEg4ejU0Zkl3a2tMSzRFDQpCcmtJNWppa05QSkJFR25zTm9zU3pWN2QNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg=="
showon="heartbeat_enabled:1" /> filter="raw" />
</fieldset> </fieldset>
</fields> </fields>
</config> </config>
@@ -21,9 +21,9 @@ use Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper;
/** /**
* MokoWaaS Health Monitor Plugin * MokoWaaS Health Monitor Plugin
* *
* Sends heartbeat data to a MokoWaaSBase control panel instance. * Sends heartbeat data to a MokoWaaSHQ control panel instance.
* The heartbeat includes site identity, version info, and optionally * Each request is RSA-signed with a private key distributed via
* the full health check payload from the core plugin. * the package manifest, verified by Base using the matching public key.
* *
* @since 02.32.00 * @since 02.32.00
*/ */
@@ -62,7 +62,6 @@ class Monitor extends CMSPlugin implements SubscriberInterface
$element = $table->element ?? ''; $element = $table->element ?? '';
// Trigger heartbeat when core or monitor plugin is saved
if (!\in_array($element, ['mokowaas', 'mokowaas_monitor'], true)) if (!\in_array($element, ['mokowaas', 'mokowaas_monitor'], true))
{ {
return; return;
@@ -77,10 +76,10 @@ class Monitor extends CMSPlugin implements SubscriberInterface
} }
/** /**
* Send heartbeat to the MokoWaaSBase control panel. * Send heartbeat to the MokoWaaSHQ control panel.
* *
* Posts site identity and version info to the MokoWaaSBase REST API. * The request is RSA-signed: the client signs domain|timestamp|token
* The control panel looks up the site by domain and verifies the token. * with its private key. Base verifies with the matching public key.
*/ */
private function sendHeartbeat(): void private function sendHeartbeat(): void
{ {
@@ -107,9 +106,9 @@ class Monitor extends CMSPlugin implements SubscriberInterface
return; return;
} }
$app = $this->getApplication(); $app = $this->getApplication();
$config = Factory::getConfig();
$config = Factory::getConfig(); $timestamp = time();
$payload = [ $payload = [
'token' => $healthToken, 'token' => $healthToken,
@@ -119,13 +118,14 @@ class Monitor extends CMSPlugin implements SubscriberInterface
'joomla_version' => (new Version())->getShortVersion(), 'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'mokowaas_version' => $this->getMokoWaaSVersion(), 'mokowaas_version' => $this->getMokoWaaSVersion(),
'timestamp' => $timestamp,
'client_info' => [ 'client_info' => [
'company' => $config->get('sitename', ''), 'company' => $config->get('sitename', ''),
'email' => $config->get('mailfrom', ''), 'email' => $config->get('mailfrom', ''),
], ],
]; ];
// Include live health data by calling the local health endpoint // Include live health data
$healthData = $this->fetchLocalHealth($siteUrl, $healthToken); $healthData = $this->fetchLocalHealth($siteUrl, $healthToken);
if ($healthData !== null) if ($healthData !== null)
@@ -133,13 +133,23 @@ class Monitor extends CMSPlugin implements SubscriberInterface
$payload['health'] = $healthData; $payload['health'] = $healthData;
} }
$endpoint = $baseUrl . '/api/index.php/v1/mokowaasbase/heartbeat'; // RSA sign the request
$headers = ['Content-Type: application/json'];
$signature = $this->signRequest($domain, $timestamp, $healthToken);
if ($signature !== null)
{
$headers[] = 'X-MokoWaaS-Signature: ' . $signature;
$headers[] = 'X-MokoWaaS-Timestamp: ' . $timestamp;
}
$endpoint = $baseUrl . '/api/index.php/v1/mokowaashq/heartbeat';
$json = json_encode($payload, JSON_UNESCAPED_SLASHES); $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
$ch = curl_init($endpoint); $ch = curl_init($endpoint);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $json, CURLOPT_POSTFIELDS => $json,
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15, CURLOPT_TIMEOUT => 15,
@@ -160,7 +170,7 @@ class Monitor extends CMSPlugin implements SubscriberInterface
{ {
$body = json_decode($response, true); $body = json_decode($response, true);
$app->enqueueMessage( $app->enqueueMessage(
'MokoWaaSBase heartbeat: ' . ($body['status'] ?? 'ok'), 'MokoWaaSHQ heartbeat: ' . ($body['status'] ?? 'ok'),
'message' 'message'
); );
} }
@@ -173,19 +183,77 @@ class Monitor extends CMSPlugin implements SubscriberInterface
'mokowaas' 'mokowaas'
); );
$app->enqueueMessage( $app->enqueueMessage(
'MokoWaaSBase heartbeat failed (HTTP ' . $code . ')', 'MokoWaaSHQ heartbeat failed (HTTP ' . $code . ')',
'warning' 'warning'
); );
} }
} }
/**
* RSA-sign the request message.
*
* @param string $domain Site domain.
* @param int $timestamp Unix timestamp.
* @param string $token Health API token.
*
* @return string|null Base64-encoded signature, or null if signing fails.
*/
private function signRequest(string $domain, int $timestamp, string $token): ?string
{
$signingKeyB64 = $this->params->get('signing_key', '');
// Fall back to manifest XML default if not yet saved in params
if (empty($signingKeyB64))
{
$manifestFile = JPATH_PLUGINS . '/system/mokowaas_monitor/mokowaas_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))
{
return null;
}
$privateKeyPem = base64_decode($signingKeyB64);
if (empty($privateKeyPem))
{
return null;
}
$message = $domain . '|' . $timestamp . '|' . $token;
$privateKey = openssl_pkey_get_private($privateKeyPem);
if ($privateKey === false)
{
return null;
}
$signature = '';
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
{
return base64_encode($signature);
}
return null;
}
/** /**
* Fetch health data from the local site's health endpoint. * Fetch health data from the local site's health endpoint.
*
* @param string $siteUrl Local site URL.
* @param string $healthToken Health API token.
*
* @return array|null Parsed health data or null on failure.
*/ */
private function fetchLocalHealth(string $siteUrl, string $healthToken): ?array private function fetchLocalHealth(string $siteUrl, string $healthToken): ?array
{ {
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description> <description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace> <namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace>
@@ -8,11 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description> <description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace> <namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version> <version>02.34.45-dev</version>
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description> <description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace> <namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
@@ -12,12 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license> <license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
<description>PLG_TASK_MOKOWAASDEMO_DESC</description> <description>PLG_TASK_MOKOWAASDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace> <namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
@@ -10,11 +10,7 @@
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php * PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
<<<<<<< HEAD:src/packages/plg_system_mokowaas/Service/DemoResetService.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.08
>>>>>>> origin/dev:source/packages/plg_task_mokowaasdemo/src/Service/DemoResetService.php
* BRIEF: Content-only snapshot/restore for demo site reset * BRIEF: Content-only snapshot/restore for demo site reset
*/ */
@@ -12,11 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license> <license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_task_mokowaassync/mokowaassync.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_task_mokowaassync/mokowaassync.xml
<description>PLG_TASK_MOKOWAASSYNC_DESC</description> <description>PLG_TASK_MOKOWAASSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace> <namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
@@ -10,11 +10,7 @@
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
<<<<<<< HEAD:src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.08
>>>>>>> origin/dev:source/packages/plg_task_mokowaassync/src/Service/ContentSyncReceiver.php
* BRIEF: Receiver-side content sync — applies incoming payload to local DB * BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/ */
@@ -10,11 +10,7 @@
* INGROUP: MokoWaaS * INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php * PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
<<<<<<< HEAD:src/packages/plg_system_mokowaas/Service/ContentSyncService.php * VERSION: 02.34.45
* VERSION: 02.34.00
=======
* VERSION: 02.34.08
>>>>>>> origin/dev:source/packages/plg_task_mokowaassync/src/Service/ContentSyncService.php
* BRIEF: Sender-side content sync builds payload and pushes to remote sites * BRIEF: Sender-side content sync builds payload and pushes to remote sites
*/ */
@@ -7,12 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<<<<<<< HEAD:src/packages/plg_webservices_mokowaas/mokowaas.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_webservices_mokowaas/mokowaas.xml
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description> <description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace> <namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files> <files>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - Perfect Publisher</name>
<author>Moko Consulting</author>
<creationDate>2026-05-28</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>
<<<<<<< HEAD:src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
<version>02.34.00</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
<folder plugin="perfectpublisher">services</folder>
<folder>src</folder>
</files>
</extension>
@@ -1,47 +0,0 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
<<<<<<< HEAD:src/packages/plg_webservices_perfectpublisher/services/provider.php
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.34.00
=======
* PATH: /source/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.34.16
>>>>>>> origin/dev:source/packages/plg_webservices_perfectpublisher/services/provider.php
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\WebServices\PerfectPublisher\Extension\PerfectPublisherApi;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new PerfectPublisherApi(
$dispatcher,
(array) PluginHelper::getPlugin('webservices', 'perfectpublisher')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -1,544 +0,0 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
<<<<<<< HEAD:src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.34.00
=======
* PATH: /source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.34.16
>>>>>>> origin/dev:source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
namespace Moko\Plugin\WebServices\PerfectPublisher\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\CMS\Router\ApiRouter;
use Joomla\Event\SubscriberInterface;
/**
* Perfect Publisher Web Services API Plugin
*
* Registers REST API routes for Perfect Publisher (com_autotweet) data.
* Provides read access to channels, posts, requests, rules, and feeds.
* Provides write access to create publish requests.
*
* Routes:
* GET /v1/perfectpublisher/channels List social channels
* GET /v1/perfectpublisher/channels/:id Get channel detail
* GET /v1/perfectpublisher/posts List published posts
* GET /v1/perfectpublisher/posts/:id Get post detail
* GET /v1/perfectpublisher/requests List pending requests
* POST /v1/perfectpublisher/requests Create a publish request
* GET /v1/perfectpublisher/rules List publishing rules
* GET /v1/perfectpublisher/feeds List RSS feeds
* GET /v1/perfectpublisher/channeltypes List channel type definitions
* GET /v1/perfectpublisher/stats Dashboard statistics
*
* @since 02.13.01
*/
final class PerfectPublisherApi extends CMSPlugin implements SubscriberInterface
{
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeApiRoute' => 'onBeforeApiRoute',
];
}
/**
* Register API routes.
*
* @param BeforeApiRouteEvent $event The API route event
*
* @return void
*/
public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
{
$router = $event->getRouter();
// All routes are handled by this plugin directly via custom callbacks
// because com_autotweet uses FOF, not standard Joomla MVC
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels',
[$this, 'getChannels']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels/:id',
[$this, 'getChannel']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts',
[$this, 'getPosts']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts/:id',
[$this, 'getPost']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/requests',
[$this, 'getRequests']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['POST'],
'v1/perfectpublisher/requests',
[$this, 'createRequest']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/rules',
[$this, 'getRules']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/feeds',
[$this, 'getFeeds']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channeltypes',
[$this, 'getChannelTypes']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/stats',
[$this, 'getStats']
)
);
}
/**
* GET /v1/perfectpublisher/channels
*
* @return void
*/
public function getChannels(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->order($db->quoteName('c.ordering') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('c.published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channels/:id
*
* @return void
*/
public function getChannel(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars, ct.description AS channeltype_desc')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('c.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Channel not found', 404);
return;
}
// Strip sensitive OAuth params
if (isset($result->params)) {
$params = json_decode($result->params, true);
if (is_array($params)) {
foreach (['access_token', 'access_secret', 'client_secret', 'api_secret', 'password'] as $key) {
if (isset($params[$key])) {
$params[$key] = '***';
}
}
$result->params = json_encode($params);
}
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/posts
*
* @return void
*/
public function getPosts(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('p.postdate') . ' DESC');
$pubstate = $app->input->get('pubstate', '');
if ($pubstate !== '') {
$query->where($db->quoteName('p.pubstate') . ' = ' . $db->quote($pubstate));
}
$channel = (int) $app->input->get('channel_id', 0);
if ($channel > 0) {
$query->where($db->quoteName('p.channel_id') . ' = ' . $channel);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/posts/:id
*
* @return void
*/
public function getPost(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name, ct.name AS channeltype_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('p.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Post not found', 404);
return;
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/requests
*
* @return void
*/
public function getRequests(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_requests'))
->order($db->quoteName('publish_up') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* POST /v1/perfectpublisher/requests
*
* Create a new publish request. Required fields: description.
* Optional: url, image_url, publish_up, plugin, priority.
*
* @return void
*/
public function createRequest(): void
{
$app = Factory::getApplication();
$db = Factory::getDbo();
$data = json_decode($app->input->json->getRaw(), true);
if (empty($data['description'])) {
$this->sendJsonError('Field "description" is required', 400);
return;
}
$now = Factory::getDate()->toSql();
$user = Factory::getUser();
$row = (object) [
'ref_id' => $data['ref_id'] ?? null,
'plugin' => $data['plugin'] ?? 'manual-api',
'priority' => (int) ($data['priority'] ?? 5),
'publish_up' => $data['publish_up'] ?? $now,
'description' => $data['description'],
'typeinfo' => (int) ($data['typeinfo'] ?? 0),
'url' => $data['url'] ?? null,
'image_url' => $data['image_url'] ?? null,
'created' => $now,
'created_by' => $user->id,
'params' => json_encode($data['params'] ?? []),
'published' => (int) ($data['published'] ?? 1),
];
$db->insertObject('#__autotweet_requests', $row, 'id');
$this->sendJsonResponse(
['id' => $row->id, 'status' => 'created'],
201
);
}
/**
* GET /v1/perfectpublisher/rules
*
* @return void
*/
public function getRules(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('r.*, rt.name AS ruletype_name, rt.description AS ruletype_desc, c.name AS channel_name')
->from($db->quoteName('#__autotweet_rules', 'r'))
->leftJoin(
$db->quoteName('#__autotweet_ruletypes', 'rt')
. ' ON ' . $db->quoteName('r.ruletype_id')
. ' = ' . $db->quoteName('rt.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('r.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('r.ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/feeds
*
* @return void
*/
public function getFeeds(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_feeds'))
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channeltypes
*
* @return void
*/
public function getChannelTypes(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_channeltypes'))
->order($db->quoteName('id') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/stats
*
* Dashboard statistics: post counts by status, channel counts, recent activity.
*
* @return void
*/
public function getStats(): void
{
$db = Factory::getDbo();
// Posts by status
$db->setQuery(
$db->getQuery(true)
->select('pubstate, COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->group($db->quoteName('pubstate'))
);
$postsByStatus = $db->loadObjectList('pubstate');
// Active channels
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_channels'))
->where($db->quoteName('published') . ' = 1')
);
$activeChannels = (int) $db->loadResult();
// Pending requests
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_requests'))
->where($db->quoteName('published') . ' = 1')
);
$pendingRequests = (int) $db->loadResult();
// Posts last 24h
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$posts24h = (int) $db->loadResult();
// Posts last 7d
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
);
$posts7d = (int) $db->loadResult();
$this->sendJsonResponse([
'posts_by_status' => $postsByStatus,
'active_channels' => $activeChannels,
'pending_requests' => $pendingRequests,
'posts_24h' => $posts24h,
'posts_7d' => $posts7d,
]);
}
/**
* Send a JSON API response.
*
* @param mixed $data Response data
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonResponse($data, int $status = 200): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['data' => $data], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
/**
* Send a JSON error response.
*
* @param string $message Error message
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonError(string $message, int $status = 400): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES);
$app->close();
}
}
+1 -8
View File
@@ -2,11 +2,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoWaaS</name> <name>Package - MokoWaaS</name>
<packagename>mokowaas</packagename> <packagename>mokowaas</packagename>
<<<<<<< HEAD:src/pkg_mokowaas.xml <version>02.34.45-dev</version>
<version>02.34.00</version>
=======
<version>02.34.15</version>
>>>>>>> origin/dev:source/pkg_mokowaas.xml
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -30,7 +26,6 @@
<file type="module" id="mod_mokowaas_cache" client="administrator">mod_mokowaas_cache.zip</file> <file type="module" id="mod_mokowaas_cache" client="administrator">mod_mokowaas_cache.zip</file>
<file type="module" id="mod_mokowaas_categories" client="administrator">mod_mokowaas_categories.zip</file> <file type="module" id="mod_mokowaas_categories" client="administrator">mod_mokowaas_categories.zip</file>
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file> <file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
<file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file> <file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file>
<file type="plugin" id="plg_task_mokowaassync" group="task">plg_task_mokowaassync.zip</file> <file type="plugin" id="plg_task_mokowaassync" group="task">plg_task_mokowaassync.zip</file>
<file type="plugin" id="plg_task_mokowaas_tickets" group="task">plg_task_mokowaas_tickets.zip</file> <file type="plugin" id="plg_task_mokowaas_tickets" group="task">plg_task_mokowaas_tickets.zip</file>
@@ -39,6 +34,4 @@
<updateservers> <updateservers>
<server type="extension" priority="1" name="Package - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml</server> <server type="extension" priority="1" name="Package - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml</server>
</updateservers> </updateservers>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
</extension> </extension>
+180 -25
View File
@@ -39,8 +39,13 @@ class Pkg_MokowaasInstallerScript
* with no default, causing INSERT failures when Joomla's package installer * with no default, causing INSERT failures when Joomla's package installer
* creates placeholder rows before processing sub-extension manifests. * creates placeholder rows before processing sub-extension manifests.
*/ */
/** @var string|null Download key saved before Joomla wipes update sites */
private ?string $savedDownloadKey = null;
public function preflight($type, $parent) public function preflight($type, $parent)
{ {
$this->saveDownloadKey();
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
@@ -101,6 +106,9 @@ class Pkg_MokowaasInstallerScript
// Clean up stale/duplicate update sites // Clean up stale/duplicate update sites
$this->cleanupStaleUpdateSites(); $this->cleanupStaleUpdateSites();
// Restore download key saved in preflight
$this->restoreDownloadKey();
// Fix orphaned update records (extension_id=0) // Fix orphaned update records (extension_id=0)
$this->fixUpdateRecords(); $this->fixUpdateRecords();
@@ -490,8 +498,7 @@ class Pkg_MokowaasInstallerScript
$db->quote('mokowaasdemo'), $db->quote('mokowaasdemo'),
$db->quote('mokowaassync'), $db->quote('mokowaassync'),
$db->quote('mokowaas_tickets'), $db->quote('mokowaas_tickets'),
$db->quote('perfectpublisher'), $db->quote('mokoonyx'),
$db->quote('mokoonyx'),
]; ];
$query = $db->getQuery(true) $query = $db->getQuery(true)
@@ -586,14 +593,16 @@ class Pkg_MokowaasInstallerScript
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml'; $dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml';
// Find all MokoWaaS update sites // Find MokoWaaS update sites (exclude MokoWaaSHQ and other Moko extensions)
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName(['update_site_id', 'location'])) ->select($db->quoteName(['update_site_id', 'location']))
->from($db->quoteName('#__update_sites')) ->from($db->quoteName('#__update_sites'))
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%') ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoWaaS%')
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')'); . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoWaaS%') . ')')
->where($db->quoteName('name') . ' NOT LIKE ' . $db->quote('%MokoWaaSHQ%'))
->where($db->quoteName('location') . ' NOT LIKE ' . $db->quote('%MokoWaaSHQ%'));
$db->setQuery($query); $db->setQuery($query);
$sites = $db->loadObjectList(); $sites = $db->loadObjectList();
@@ -656,6 +665,69 @@ class Pkg_MokowaasInstallerScript
} }
} }
/**
* Backup all non-empty extra_query values from update sites.
*
* @return array Map of update_site_id => extra_query
*/
private function saveDownloadKey(): void
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('us.extra_query'))
->from($db->quoteName('#__update_sites', 'us'))
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas'))
->setLimit(1)
);
$key = $db->loadResult();
if (!empty($key))
{
$this->savedDownloadKey = $key;
}
}
catch (\Throwable $e) {}
}
private function restoreDownloadKey(): void
{
if ($this->savedDownloadKey === null)
{
return;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('us.update_site_id'))
->from($db->quoteName('#__update_sites', 'us'))
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas'))
->setLimit(1)
);
$siteId = (int) $db->loadResult();
if ($siteId > 0)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
->where($db->quoteName('update_site_id') . ' = ' . $siteId)
)->execute();
}
}
catch (\Throwable $e) {}
}
/** /**
* Ensure the MokoWaaS update server entry stays enabled and points * Ensure the MokoWaaS update server entry stays enabled and points
* to the correct dynamic endpoint with the license key attached. * to the correct dynamic endpoint with the license key attached.
@@ -714,42 +786,125 @@ class Pkg_MokowaasInstallerScript
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
// Get health token from core plugin
$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')) ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->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'));
$params = json_decode((string) $db->setQuery($query)->loadResult()); $coreParams = json_decode((string) $db->setQuery($query)->loadResult());
$healthToken = $coreParams->health_api_token ?? '';
$healthToken = $params->health_api_token ?? '';
if (empty($healthToken)) if (empty($healthToken))
{ {
return; return;
} }
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); // Get base URL and signing key from monitor plugin
$siteName = Factory::getConfig()->get('sitename', 'Joomla'); $query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_monitor'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$monitorParams = json_decode((string) $db->setQuery($query)->loadResult());
$baseUrl = rtrim($monitorParams->base_url ?? '', '/');
// Fall back to manifest XML default if not yet saved in params
if (empty($baseUrl))
{
$manifestFile = JPATH_PLUGINS . '/system/mokowaas_monitor/mokowaas_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))
{
return;
}
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
$timestamp = time();
$payload = json_encode([ $payload = json_encode([
'site_url' => $siteUrl, 'token' => $healthToken,
'site_name' => $siteName, 'domain' => $domain,
'health_token' => $healthToken, 'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
'action' => 'register', 'site_url' => $siteUrl,
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'timestamp' => $timestamp,
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
$ch = curl_init('https://bench.mokoconsulting.tech/api/waas-heartbeat/register'); $headers = ['Content-Type: application/json'];
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [ // RSA sign the request — fall back to manifest XML default
'Content-Type: application/json', $signingKeyB64 = $monitorParams->signing_key ?? '';
'X-MokoWaaS-Key: moko-waas-hb-2026-x9k4m',
if (empty($signingKeyB64))
{
$manifestFile = JPATH_PLUGINS . '/system/mokowaas_monitor/mokowaas_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-MokoWaaS-Signature: ' . base64_encode($signature);
$headers[] = 'X-MokoWaaS-Timestamp: ' . $timestamp;
}
}
}
$endpoint = $baseUrl . '/api/index.php/v1/mokowaashq/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,
]); ]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch); $response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -757,7 +912,7 @@ class Pkg_MokowaasInstallerScript
if ($code >= 200 && $code < 300) if ($code >= 200 && $code < 300)
{ {
Factory::getApplication()->enqueueMessage('Grafana heartbeat: site registered', 'message'); Factory::getApplication()->enqueueMessage('MokoWaaSHQ heartbeat: site registered', 'message');
} }
} }
catch (\Throwable $e) catch (\Throwable $e)
-48
View File
@@ -1,48 +0,0 @@
<?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: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.32.04
PATH: /mokowaas.xml
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
-->
<extension type="component" method="upgrade">
<name>MokoWaaS</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.00</version>
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoWaaS</namespace>
<administration>
<menu img="class:cogs">MokoWaaS</menu>
<files folder="admin">
<folder>language</folder>
<folder>services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
</administration>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
<media destination="com_mokowaas" folder="media">
<folder>css</folder>
<folder>js</folder>
</media>
</extension>
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.00
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
class AllowedIpsField extends FormField
{
protected $type = 'AllowedIps';
protected function getInput()
{
$config = Factory::getApplication()->getConfig();
$allowedRaw = $config->get('mokowaas_allowed_ips', '');
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (empty($allowedRaw))
{
$status = '<span class="badge bg-danger">Not configured</span>';
$ipList = '<em>No IPs set — emergency access is blocked.</em>';
}
else
{
$ips = array_map('trim', explode(',', $allowedRaw));
$status = '<span class="badge bg-success">'
. count($ips) . ' IP(s) configured</span>';
$ipItems = [];
foreach ($ips as $ip)
{
$match = ($ip === $currentIp)
? ' <span class="badge bg-info">your IP</span>'
: '';
$ipItems[] = '<code>' . htmlspecialchars($ip)
. '</code>' . $match;
}
$ipList = implode(', ', $ipItems);
}
$yourIp = '<code>' . htmlspecialchars($currentIp) . '</code>';
return '<div class="alert alert-info mb-0">'
. '<strong>IP Whitelist:</strong> ' . $status . '<br>'
. '<strong>Allowed IPs:</strong> ' . $ipList . '<br>'
. '<strong>Your current IP:</strong> ' . $yourIp . '<br>'
. '<small class="text-muted">Set <code>public '
. '$mokowaas_allowed_ips = \'1.2.3.4,5.6.7.8\';</code>'
. ' in configuration.php to change.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -1,40 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.00
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
class CurrentIpField extends FormField
{
protected $type = 'CurrentIp';
protected function getInput()
{
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return '<div class="alert alert-info mb-0 py-2">'
. '<strong>Your current IP:</strong> '
. '<code>' . htmlspecialchars($currentIp) . '</code> '
. '<small class="text-muted">&mdash; add this to the table below to keep your session alive.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -1,237 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.00
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Router\Route;
/**
* Displays the demo reset scheduled task status: schedule, next run,
* last run, and a direct link to edit the task in Joomla's Scheduler.
*
* @since 02.29.00
*/
class DemoTaskInfoField extends FormField
{
protected $type = 'DemoTaskInfo';
protected function getInput()
{
// Query the scheduled task — if it exists and is enabled, demo mode is on
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
$newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add');
if (!$task)
{
return '<div class="alert alert-info mb-0 py-2">'
. 'No demo reset task configured. '
. '<a href="' . $newTaskLink . '" class="alert-link">Create a Scheduled Task</a> '
. 'and select <strong>MokoWaaS Demo Reset</strong> to enable demo mode.</div>';
}
$taskId = (int) $task['id'];
$state = (int) $task['state'];
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
// Parse schedule from execution_rules
$rules = json_decode($task['execution_rules'] ?? '{}', true);
$ruleType = $rules['rule-type'] ?? '';
switch ($ruleType)
{
case 'cron-expression':
$schedule = $rules['cron-expression'] ?? '';
$friendlySchedule = $this->friendlySchedule($schedule);
break;
case 'interval-minutes':
$mins = (int) ($rules['interval-minutes'] ?? 0);
if ($mins >= 1440 && $mins % 1440 === 0)
{
$days = $mins / 1440;
$schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : '');
}
elseif ($mins >= 60 && $mins % 60 === 0)
{
$hours = $mins / 60;
$schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : '');
}
else
{
$schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : '');
}
$friendlySchedule = $schedule;
break;
case 'interval-hours':
$hours = (int) ($rules['interval-hours'] ?? 0);
$schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
case 'interval-days':
$days = (int) ($rules['interval-days'] ?? 0);
$schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
default:
$schedule = $ruleType ?: 'Not set';
$friendlySchedule = 'Custom';
}
// Next execution
$nextExec = $task['next_execution'] ?? '';
$nextFormatted = 'Not scheduled';
$nextBadge = '';
if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($nextExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$nextFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$nextFormatted = $nextExec;
}
$diff = strtotime($nextExec . ' UTC') - time();
if ($diff <= 0)
{
$nextBadge = '<span class="badge bg-warning text-dark">DUE</span>';
}
elseif ($diff < 3600)
{
$nextBadge = '<span class="badge bg-info">in ' . (int) ceil($diff / 60) . ' min</span>';
}
elseif ($diff < 86400)
{
$nextBadge = '<span class="badge bg-info">in ' . round($diff / 3600, 1) . 'h</span>';
}
else
{
$nextBadge = '<span class="badge bg-secondary">in ' . round($diff / 86400, 1) . 'd</span>';
}
}
// Last execution
$lastExec = $task['last_execution'] ?? '';
$lastFormatted = 'Never';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($lastExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$lastFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$lastFormatted = $lastExec;
}
}
// State badge
$stateBadge = $state === 1
? '<span class="badge bg-success">Enabled</span>'
: '<span class="badge bg-danger">Disabled</span>';
// Link to edit the task
$editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId);
// Task params — default to On when keys are missing (matches form defaults)
$taskParams = json_decode($task['params'] ?? '{}', true) ?: [];
$bannerOn = !isset($taskParams['banner_enabled']) || (int) $taskParams['banner_enabled'] === 1;
$mediaOn = !isset($taskParams['include_media']) || (int) $taskParams['include_media'] === 1;
$countdownOn = !isset($taskParams['show_countdown']) || (int) $taskParams['show_countdown'] === 1;
// Check if snapshot exists
$snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default');
// Build info card
return '<div class="card card-body bg-light py-2 px-3 mb-0">'
. '<table class="table table-sm table-borderless mb-1" style="max-width:550px">'
. '<tr><td class="text-muted" style="width:130px">Status</td><td>' . $stateBadge . '</td></tr>'
. '<tr><td class="text-muted">Schedule</td><td>' . htmlspecialchars($friendlySchedule) . '</td></tr>'
. '<tr><td class="text-muted">Next Reset</td><td>' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '</td></tr>'
. '<tr><td class="text-muted">Last Reset</td><td>' . htmlspecialchars($lastFormatted) . '</td></tr>'
. '<tr><td class="text-muted">Runs</td><td>' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed</td></tr>'
. '<tr><td class="text-muted">Baseline</td><td>' . ($snapshotExists ? '<span class="badge bg-success">Saved</span>' : '<span class="badge bg-warning text-dark">Not taken yet</span>') . '</td></tr>'
. '<tr><td class="text-muted">Banner</td><td>' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '</td></tr>'
. '<tr><td class="text-muted">Images</td><td>' . ($mediaOn ? 'Included' : 'Excluded') . '</td></tr>'
. '</table>'
. '<a href="' . $editLink . '" class="btn btn-sm btn-outline-primary">'
. '<span class="icon-cog" aria-hidden="true"></span> Manage Scheduled Task</a>'
. '</div>';
}
protected function getLabel()
{
return '<label class="form-label"><strong>Scheduled Reset</strong></label>';
}
/**
* Convert a cron expression to a human-readable string.
*
* @param string $cron Cron expression
*
* @return string
*/
private function friendlySchedule(string $cron): string
{
$map = [
'* * * * *' => 'Every minute',
'*/5 * * * *' => 'Every 5 minutes',
'*/15 * * * *' => 'Every 15 minutes',
'*/30 * * * *' => 'Every 30 minutes',
'0 */1 * * *' => 'Every hour',
'0 */4 * * *' => 'Every 4 hours',
'0 */6 * * *' => 'Every 6 hours',
'0 */12 * * *' => 'Every 12 hours',
'0 0 * * *' => 'Daily at midnight',
'0 6 * * *' => 'Daily at 6:00 AM',
'0 0 * * 0' => 'Weekly (Sunday)',
'0 0 1 * *' => 'Monthly (1st)',
];
return $map[$cron] ?? 'Custom';
}
}
@@ -1,156 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.00
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Pulls the next execution time directly from the Joomla scheduled task
* (#__scheduler_tasks) and displays it formatted in the site timezone.
*
* @since 02.29.00
*/
class NextResetField extends FormField
{
protected $type = 'NextReset';
protected function getInput()
{
// Check if demo mode is enabled
$demoEnabled = false;
if ($this->form)
{
$demoEnabled = (int) $this->form->getValue('demo_mode_enabled', 'params', 0) === 1;
}
if (!$demoEnabled)
{
return '<span class="form-control-plaintext text-muted">Demo mode is off</span>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Query the actual next_execution from the scheduled task
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([
$db->quoteName('next_execution'),
$db->quoteName('last_execution'),
$db->quoteName('state'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
if (!$task)
{
return '<div class="alert alert-secondary mb-0 py-2">No scheduled task found — save to create one automatically.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
if ((int) $task['state'] !== 1)
{
return '<div class="alert alert-warning mb-0 py-2">Scheduled task is disabled.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
$nextExec = $task['next_execution'];
$lastExec = $task['last_execution'];
if (empty($nextExec) || $nextExec === '0000-00-00 00:00:00')
{
return '<div class="alert alert-secondary mb-0 py-2">Waiting for first run...</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Convert to site timezone
$utcTimestamp = strtotime($nextExec);
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
try
{
$dt = new \DateTime('@' . $utcTimestamp);
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
}
catch (\Throwable $e)
{
$formatted = $nextExec . ' UTC';
}
// Relative time
$diff = $utcTimestamp - time();
$relative = '';
if ($diff <= 0)
{
$relative = '<span class="badge bg-warning text-dark">overdue</span>';
}
elseif ($diff < 3600)
{
$mins = (int) ceil($diff / 60);
$relative = '<span class="badge bg-info">in ' . $mins . ' min</span>';
}
elseif ($diff < 86400)
{
$hours = round($diff / 3600, 1);
$relative = '<span class="badge bg-info">in ' . $hours . 'h</span>';
}
else
{
$days = round($diff / 86400, 1);
$relative = '<span class="badge bg-secondary">in ' . $days . 'd</span>';
}
// Last run info
$lastInfo = '';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$lastDt = new \DateTime($lastExec);
$lastDt->setTimezone(new \DateTimeZone($siteTimezone));
$lastInfo = '<small class="text-muted ms-2">Last run: ' . $lastDt->format('M j, g:i A') . '</small>';
}
catch (\Throwable $e)
{
// skip
}
}
return '<div class="d-flex align-items-center gap-2 flex-wrap">'
. '<span class="form-control-plaintext" style="font-weight:500">'
. '<span class="icon-calendar" aria-hidden="true"></span> '
. htmlspecialchars($formatted) . '</span> '
. $relative
. $lastInfo
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($nextExec) . '" />'
. '</div>';
}
}
@@ -1,175 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.00
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Renders a multi-select list box of all Joomla database tables, with
* content-related tables pre-selected by default.
*
* @since 02.26.00
*/
class SnapshotTablesField extends FormField
{
protected $type = 'SnapshotTables';
/**
* Tables selected by default when no value is stored yet.
*
* @var array
* @since 02.25.00
*/
private const DEFAULT_TABLES = [
'#__content',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__menu',
'#__menu_types',
'#__modules',
'#__modules_menu',
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
'#__tags',
'#__contentitem_tag_map',
'#__assets',
];
/**
* Table suffixes grouped by category.
*
* @var array
* @since 02.25.00
*/
private const TABLE_GROUPS = [
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'Menus' => ['menu', 'menu_types'],
'Modules' => ['modules', 'modules_menu'],
'Assets' => ['assets'],
];
protected function getInput()
{
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$tables = $db->getTableList();
// Resolve selected values
$selected = $this->value;
if ($selected === null || $selected === '')
{
$selected = self::DEFAULT_TABLES;
}
elseif (is_string($selected))
{
$selected = array_filter(array_map('trim', explode("\n", $selected)));
}
$selected = (array) $selected;
// Flatten nested arrays from broken save format [["#__content"],["#__categories"]]
$selected = array_map(function ($v) {
return is_array($v) ? reset($v) : $v;
}, $selected);
// Group tables
$grouped = [];
foreach ($tables as $table)
{
if (strpos($table, $prefix) !== 0)
{
continue;
}
$suffix = substr($table, strlen($prefix));
$logical = '#__' . $suffix;
$group = 'Other';
foreach (self::TABLE_GROUPS as $groupName => $patterns)
{
if (in_array($suffix, $patterns, true))
{
$group = $groupName;
break;
}
}
$grouped[$group][] = $logical;
}
// Build HTML select with optgroups
$size = (int) ($this->element['size'] ?? 15);
$html = '<select name="' . $this->name . '" id="' . $this->id . '"'
. ' multiple="multiple" size="' . $size . '"'
. ' class="form-select">';
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
foreach ($priority as $g)
{
if (!empty($grouped[$g]))
{
$html .= '<optgroup label="' . $g . '">';
foreach ($grouped[$g] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
unset($grouped[$g]);
}
}
if (!empty($grouped['Other']))
{
$html .= '<optgroup label="Other">';
foreach ($grouped['Other'] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
}
$html .= '</select>';
// "Reset to defaults" link
$defaultsJson = htmlspecialchars(json_encode(self::DEFAULT_TABLES), ENT_QUOTES, 'UTF-8');
$html .= '<div class="mt-1">'
. '<a href="#" class="small" onclick="'
. 'var sel=document.getElementById(\'' . $this->id . '\');'
. 'var defs=' . $defaultsJson . ';'
. 'Array.from(sel.options).forEach(function(o){o.selected=defs.indexOf(o.value)!==-1;});'
. 'return false;'
. '"><span class="icon-refresh" aria-hidden="true"></span> Reset to defaults</a>'
. '</div>';
return $html;
}
}
@@ -1,260 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License (./LICENSE.md).
# FILE INFORMATION
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.04
PATH: /src/mokowaas.xml
BRIEF: Plugin manifest for MokoWaaS system plugin
NOTE: Defines installation metadata, files, and configuration for Joomla
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS</name>
<element>mokowaas</element>
<author>Moko Consulting</author>
<creationDate>2026-05-22</creationDate>
<copyright>Copyright (C) 2025 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.00</version>
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
<files>
<filename plugin="mokowaas">script.php</filename>
<folder>Extension</folder>
<folder>Field</folder>
<folder>Helper</folder>
<folder>Service</folder>
<folder>forms</folder>
<folder>payload</folder>
<folder>services</folder>
<folder>language</folder>
<folder>administrator</folder>
</files>
<media destination="plg_system_mokowaas" folder="media">
<filename>index.html</filename>
<filename>favicon.ico</filename>
<filename>favicon.svg</filename>
<filename>favicon_256.png</filename>
<filename>logo.png</filename>
</media>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas.ini</language>
<language tag="en-US">en-US/plg_system_mokowaas.ini</language>
</languages>
<languages folder="administrator/language">
<language tag="en-GB">en-GB/plg_system_mokowaas.sys.ini</language>
<language tag="en-US">en-US/plg_system_mokowaas.sys.ini</language>
</languages>
<administration>
<files folder="administrator">
<folder>language</folder>
</files>
</administration>
<config>
<fields name="params"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<fieldset name="basic">
<field
name="health_api_token"
type="CopyableToken"
label="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC"
default=""
filter="raw"
readonly="true"
/>
<field name="dev_mode" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="reset_hits"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC"
default="0"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="delete_versions"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC"
default="0"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="tenant_restrictions"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC"
>
<field name="restrict_installer" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL"
description="PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="allow_extension_updates" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC"
class="btn-group btn-group-yesno"
showon="restrict_installer:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="hide_sysinfo" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="restrict_global_config" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL"
description="PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="restrict_template_editing" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="disable_install_url" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="hidden_menu_items" type="textarea"
label="PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC"
rows="5" filter="raw" />
</fieldset>
<fieldset name="demo_mode"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field name="demo_scheduled_task" type="DemoTaskInfo"
label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL"
/>
</fieldset>
<fieldset name="security"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field
name="emergency_access"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="allowed_ips_display"
type="AllowedIps"
label=""
/>
<field name="force_https" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="admin_session_timeout" type="number"
label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC"
default="60" hint="Minutes (0 = Joomla default)" />
<field
name="current_ip_display"
type="CurrentIp"
label=""
/>
<field
name="trusted_ips"
type="subform"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC"
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move"
/>
<field name="password_min_length" type="number" default="12"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL"
description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" />
<field name="password_require_uppercase" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="password_require_number" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="password_require_special" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="upload_allowed_types" type="text"
label="PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC"
default="jpg,jpeg,png,gif,webp,svg,pdf,doc,docx,xls,xlsx" />
<field name="upload_max_size_mb" type="number"
label="PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC"
default="100" />
</fieldset>
</fields>
</config>
</extension>