Compare commits

...

29 Commits

Author SHA1 Message Date
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
Jonathan Miller 55ec926fdc chore: remove updates.xml references from CLAUDE.md
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
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: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Successful in 18s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 34s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
updates.xml is deprecated — MokoGitea generates feeds dynamically.
2026-06-06 11:26:51 -05:00
Jonathan Miller b4f916addb chore: remove deprecated updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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
MokoGitea generates update feeds dynamically from releases.
Static updates.xml is no longer needed.
2026-06-06 11:16:51 -05:00
Jonathan Miller a51f04c841 feat: provision reset API for new client setup from Base
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
ProvisionController: POST /api/v1/mokowaas/provision-reset
- Resets article hits to zero
- Deletes content version history
- Regenerates heartbeat token (optional, for breach response)
- Revokes all user API tokens with email notification (optional)
- Sets setup-required flag for new client info collection

Core plugin: checkSetupRequired() shows persistent admin banner
until plugin settings are saved. Clears flag on save.

Route registered in webservices plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 11:10:02 -05:00
Jonathan Miller db2ed26e65 feat: heartbeat sends full health payload and client info
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 24s
Monitor plugin now includes live health data (16 checks) and client
info (company name, email from Joomla config) in the heartbeat payload.
Health data is fetched via local HTTP call to /?mokowaas=health to
reuse existing auth and check logic without internal coupling.

MokoWaaSBase can use client_info for auto-contact creation and the
health payload for dashboard visualization without separate polling.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 11:01:33 -05:00
76 changed files with 2691 additions and 774 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ Joomla **package** (`pkg_mokowaas`) with 17 sub-extensions:
### Update Server
`updates.xml` is stored in the repo root and maintained manually. Points to ZIP assets on Gitea releases.
MokoGitea generates update feeds dynamically from releases — no static `updates.xml` needed.
## Source Directory
+1 -1
View File
@@ -9,7 +9,7 @@
<display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.34.16</version>
<version>02.34.26</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+324 -316
View File
@@ -1,316 +1,324 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
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
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found - aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: "Publish stable release"
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Update release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
else
NOTES="Stable release"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral - created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
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}"
# Create version/XX.YY.ZZ from main
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"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
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
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
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
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: "Publish stable release"
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
else
NOTES="Stable release"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
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}"
# Create version/XX.YY.ZZ from main
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"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
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
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
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 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 02.34.16
# VERSION: 02.34.26
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+243 -241
View File
@@ -1,241 +1,243 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
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_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# 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
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# 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
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--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
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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"
git add -A
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" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
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 ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
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)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $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
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
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_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# 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/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# 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
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--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
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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"
git add -A
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" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
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 ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
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)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $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
+31 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
VERSION: 02.34.16
VERSION: 02.34.26
BRIEF: Version history using `Keep a Changelog`
-->
@@ -22,6 +22,36 @@
## [Unreleased]
## [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 MokoWaaSBase 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 MokoWaaSBase
- 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
- Database Tools view — table status, optimize, repair, session purge (#127)
- Cache Cleanup view — directory size reporting and one-click cleanup (#128)
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md
VERSION: 02.34.16
VERSION: 02.34.26
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.34.16
VERSION: 02.34.26
BRIEF: Security vulnerability reporting and handling policy
-->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoWaaS Build Guide (VERSION: 02.34.16)
# MokoWaaS Build Guide (VERSION: 02.34.26)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoWaaS Configuration Guide (VERSION: 02.34.16)
# MokoWaaS Configuration Guide (VERSION: 02.34.26)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set
-->
# MokoWaaS Installation Guide (VERSION: 02.34.16)
# MokoWaaS Installation Guide (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoWaaS Operations Guide (VERSION: 02.34.16)
# MokoWaaS Operations Guide (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance
-->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.16)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoWaaS Testing Guide (VERSION: 02.34.16)
# MokoWaaS Testing Guide (VERSION: 02.34.26)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams
-->
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.16)
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.16)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.34.16
VERSION: 02.34.26
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoWaaS Documentation Index (VERSION: 02.34.16)
# MokoWaaS Documentation Index (VERSION: 02.34.26)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md
VERSION: 02.34.16
VERSION: 02.34.26
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoWaaS Plugin Overview (VERSION: 02.34.16)
# MokoWaaS Plugin Overview (VERSION: 02.34.26)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md
VERSION: 02.34.16
VERSION: 02.34.26
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"?>
<!--
Extension catalog for MokoWaaS Extension Manager.
Each entry points to the extension's own updates.xml — the installer
resolves the latest version and download URL at runtime.
Each entry points to the extension's own updates.xml. The installer
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.
The updateserver URL must point to a valid Joomla updates.xml file.
-->
<catalog>
<extension>
@@ -19,6 +19,16 @@
<protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoWaaSBase</name>
<element>pkg_mokowaasbase</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/MokoWaaSBase/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoOnyx</name>
<element>mokoonyx</element>
@@ -30,14 +40,24 @@
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomTOS</name>
<element>com_mokojoomtos</element>
<type>component</type>
<description>Terms of Service and privacy policy component with consent tracking.</description>
<icon>icon-file-contract</icon>
<category>Components</category>
<article>https://mokoconsulting.tech/support/products/mokojoomtos</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/dev/updates.xml</updateserver>
<name>MokoJoomOpenGraph</name>
<element>pkg_mokoog</element>
<type>package</type>
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
<icon>icon-share-alt</icon>
<category>SEO</category>
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>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>
<name>MokoJoomHero</name>
@@ -50,14 +70,34 @@
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoWaaS Announce</name>
<element>mod_mokowaas_announce</element>
<name>MokoJoomCommunity</name>
<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>
<description>Centralized announcement system via admin module.</description>
<icon>icon-bullhorn</icon>
<description>Store locator module with Google Maps integration and search.</description>
<icon>icon-map-marker-alt</icon>
<category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokowaas-announce</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaSAnnounce/raw/branch/dev/updates.xml</updateserver>
<article>https://mokoconsulting.tech/support/products/mokojoomstorelocator</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>DPCalendar API</name>
@@ -79,14 +119,4 @@
<article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomOpenGraph</name>
<element>pkg_mokoog</element>
<type>package</type>
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
<icon>icon-share-alt</icon>
<category>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>
@@ -133,3 +133,19 @@ INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `rete
(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)'),
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
--
-- Download Key Storage — persistent backup of extension download keys
-- that survives Joomla update site recreation
--
CREATE TABLE IF NOT EXISTS `#__mokowaas_download_keys` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`element` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'Extension element name',
`location` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Update server URL',
`dlid` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Download key value',
`created` DATETIME NOT NULL,
`modified` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_dlkey_element` (`element`),
KEY `idx_dlkey_location` (`location`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -48,6 +48,12 @@ class ExtensionsModel extends BaseDatabaseModel
$remoteVersion = $release['version'] ?? '';
$downloadUrl = $release['download_url'] ?? '';
// Skip extensions with no release available and not installed
if (empty($remoteVersion) && $localVersion === null)
{
continue;
}
$status = 'not_installed';
if ($localVersion !== null)
@@ -62,6 +68,9 @@ class ExtensionsModel extends BaseDatabaseModel
$extensionId = $this->getExtensionId($entry['element']);
$needsDlid = $release['needs_dlid'] ?? false;
$hasDlid = $needsDlid && $extensionId ? $this->hasDownloadKey($entry['element']) : false;
$packages[] = (object) [
'label' => $entry['name'],
'description' => $entry['description'],
@@ -76,6 +85,9 @@ class ExtensionsModel extends BaseDatabaseModel
'article_url' => $entry['article'] ?? '',
'protected' => ($entry['protected'] ?? 'false') === 'true',
'extension_id' => $extensionId,
'needs_dlid' => $needsDlid,
'has_dlid' => $hasDlid,
'has_stable' => $release['has_stable'] ?? false,
];
}
@@ -226,13 +238,36 @@ class ExtensionsModel extends BaseDatabaseModel
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';
$downloadUrl = '';
$needsDlid = false;
foreach ($xml->update as $update)
{
$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, '<='))
{
@@ -241,10 +276,15 @@ class ExtensionsModel extends BaseDatabaseModel
$bestVersion = $ver;
// Get download URL from <downloads><downloadurl>
if (isset($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 [
'version' => $bestVersion,
'download_url' => $downloadUrl,
'has_stable' => $hasStable,
'has_dev' => $hasDev,
'needs_dlid' => $needsDlid,
];
}
@@ -299,6 +342,166 @@ class ExtensionsModel extends BaseDatabaseModel
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;
}
}
/**
* Save a download key for a Moko extension.
*
* @param string $element Extension element name.
* @param string $dlid Download key value.
* @param string $location Update server URL (optional).
*/
public function saveDownloadKey(string $element, string $dlid, string $location = ''): void
{
$db = $this->getDatabase();
$now = gmdate('Y-m-d H:i:s');
// Upsert — update if exists, insert if not
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
);
if ((int) $db->loadResult() > 0)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_download_keys'))
->set($db->quoteName('dlid') . ' = ' . $db->quote($dlid))
->set($db->quoteName('location') . ' = ' . $db->quote($location))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
)->execute();
}
else
{
$db->setQuery(
$db->getQuery(true)
->insert($db->quoteName('#__mokowaas_download_keys'))
->columns([$db->quoteName('element'), $db->quoteName('location'), $db->quoteName('dlid'), $db->quoteName('created'), $db->quoteName('modified')])
->values(implode(',', [
$db->quote($element),
$db->quote($location),
$db->quote($dlid),
$db->quote($now),
$db->quote($now),
]))
)->execute();
}
// Immediately apply to Joomla's update site
$this->applyDownloadKey($element, $dlid);
}
/**
* Apply a stored download key to Joomla's update site for an extension.
*/
public function applyDownloadKey(string $element, string $dlid): void
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('us.' . $db->quoteName('update_site_id'))
->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))
);
$siteIds = $db->loadColumn() ?: [];
foreach ($siteIds as $siteId)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $dlid))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
)->execute();
}
}
/**
* Re-apply all stored Moko download keys to Joomla's update sites.
* Called after updates that may have wiped extra_query.
*
* @return int Number of keys re-applied.
*/
public static function reapplyAllDownloadKeys(): int
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('dlid') . ' != ' . $db->quote(''))
);
$keys = $db->loadObjectList() ?: [];
$applied = 0;
foreach ($keys as $key)
{
$db->setQuery(
$db->getQuery(true)
->select('us.' . $db->quoteName('update_site_id'))
->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($key->element))
);
$siteIds = $db->loadColumn() ?: [];
foreach ($siteIds as $siteId)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $key->dlid))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
)->execute();
$applied++;
}
}
return $applied;
}
catch (\Throwable $e)
{
return 0;
}
}
/**
* Get the extension_id for an element (for uninstall links).
*
@@ -44,7 +44,7 @@ $statusBadge = [
<?php
$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-body d-flex flex-column">
<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>
<?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="small text-muted">
<?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-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
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>
Update to <?php echo htmlspecialchars($pkg->remote_version); ?>
</button>
@@ -91,7 +101,9 @@ $statusBadge = [
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
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>
Install
</button>
@@ -150,15 +162,37 @@ document.addEventListener('DOMContentLoaded', function() {
var token = el.dataset.token;
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;
el.disabled = true;
var origHtml = el.textContent;
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();
fd.append('download_url', downloadUrl);
fd.append('download_url', finalUrl);
fd.append(token, '1');
if (dlid) {
fd.append('dlid', dlid.trim());
fd.append('element', el.dataset.element || '');
}
fetch(url, {
method: 'POST',
@@ -0,0 +1,236 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Provision reset API controller.
*
* POST /api/index.php/v1/mokowaas/provision-reset
*
* Resets a site for new client provisioning: clears hits, versions,
* download keys, and flags the site for fresh client info collection.
* Used after copying a demo site to create a new client install.
*
* @since 02.35.00
*/
class ProvisionController extends BaseController
{
/**
* Reset the site for new client provisioning.
*
* @return void
*/
public function execute($task = 'provision'): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokowaas'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$db = Factory::getDbo();
$results = [];
// 1. Reset article hit counters
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('hits') . ' = 0')
)->execute();
$results['hits_reset'] = $db->getAffectedRows();
}
catch (\Throwable $e)
{
$results['hits_reset'] = 'error: ' . $e->getMessage();
}
// 2. Delete content version history
try
{
$db->setQuery(
$db->getQuery(true)->delete($db->quoteName('#__history'))
)->execute();
$results['versions_deleted'] = $db->getAffectedRows();
}
catch (\Throwable $e)
{
$results['versions_deleted'] = 'error: ' . $e->getMessage();
}
// 3. Regenerate heartbeat token if requested
$input = $app->getInput()->json;
$resetToken = (bool) ($input->get('reset_token', false, 'BOOLEAN'));
if ($resetToken)
{
try
{
$newToken = bin2hex(random_bytes(32));
$plugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas');
if ($plugin)
{
$pluginParams = new \Joomla\Registry\Registry($plugin->params);
$pluginParams->set('health_api_token', $newToken);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($pluginParams->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
$results['token_regenerated'] = true;
$results['new_token'] = $newToken;
}
}
catch (\Throwable $e)
{
$results['token_regenerated'] = 'error: ' . $e->getMessage();
}
}
// 4. Reset all user API tokens if requested
$resetApiTokens = (bool) ($input->get('reset_api_tokens', false, 'BOOLEAN'));
if ($resetApiTokens)
{
try
{
// Get users who have API tokens before deleting
$db->setQuery(
$db->getQuery(true)
->select('DISTINCT ' . $db->quoteName('user_id'))
->from($db->quoteName('#__user_keys'))
->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
);
$affectedUserIds = $db->loadColumn() ?: [];
$db->setQuery(
$db->getQuery(true)->delete($db->quoteName('#__user_keys'))
->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
)->execute();
$results['api_tokens_revoked'] = $db->getAffectedRows();
// Notify affected users
if (!empty($affectedUserIds))
{
$this->notifyTokenReset($db, $affectedUserIds);
$results['users_notified'] = \count($affectedUserIds);
}
}
catch (\Throwable $e)
{
$results['api_tokens_revoked'] = 'error: ' . $e->getMessage();
}
}
// 5. Flag site for fresh client info setup
try
{
// Write a flag file that the core plugin checks on next admin load
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
file_put_contents($flagFile, json_encode([
'created' => gmdate('Y-m-d\TH:i:s\Z'),
'reason' => 'provision-reset',
'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
]));
$results['setup_flag'] = true;
}
catch (\Throwable $e)
{
$results['setup_flag'] = 'error: ' . $e->getMessage();
}
$this->sendJson(200, [
'status' => 'ok',
'message' => 'Site provisioned for new client.',
'results' => $results,
]);
}
/**
* Notify users that their API tokens have been revoked.
*/
private function notifyTokenReset($db, array $userIds): void
{
try
{
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('name'), $db->quoteName('email')])
->from($db->quoteName('#__users'))
->whereIn($db->quoteName('id'), $userIds)
->where($db->quoteName('block') . ' = 0')
);
$users = $db->loadObjectList() ?: [];
$config = Factory::getConfig();
$siteName = $config->get('sitename', 'Joomla');
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$mailer = Factory::getMailer();
foreach ($users as $u)
{
try
{
$mailer->clearAllRecipients();
$mailer->addRecipient($u->email, $u->name);
$mailer->setSubject($siteName . ' — API tokens have been reset');
$mailer->setBody(
"Hello {$u->name},\n\n"
. "Your API access tokens on {$siteName} have been revoked by an administrator.\n\n"
. "If you use API integrations, please log in and generate a new token:\n"
. "{$siteUrl}/administrator/\n\n"
. "{$siteName}"
);
$mailer->send();
}
catch (\Throwable $e)
{
// Non-critical
}
}
}
catch (\Throwable $e)
{
// Non-critical
}
}
private function sendJson(int $code, array $data): void
{
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_SLASHES);
Factory::getApplication()->close();
}
}
@@ -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);
});
});
}
});
+1 -1
View File
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-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>
<namespace path="src">Moko\Component\MokoWaaS</namespace>
@@ -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>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>MOD_MOKOWAAS_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCategories</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<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>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.16
* VERSION: 02.34.26
* PATH: /src/Extension/MokoWaaS.php
* NOTE: Core system plugin for MokoWaaS admin tools suite
*/
@@ -167,10 +167,11 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
$this->handleMokoApi($mokoAction);
}
// One-time remote login (admin only)
// Admin-only features
if ($this->app->isClient('administrator'))
{
$this->handleOneTimeLogin();
$this->checkSetupRequired();
$this->preserveDownloadKeys();
}
}
@@ -242,7 +243,14 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
// Grafana auto-provisioning
$this->handleGrafanaProvisioning($params, $app);
// NOTE: reset_hits and delete_versions now handled by devtools plugin
// Clear setup-required flag on save (new client setup complete)
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
if (file_exists($flagFile))
{
@unlink($flagFile);
$app->enqueueMessage('Client setup complete — setup flag cleared.', 'message');
}
if ($changed)
{
@@ -2053,6 +2061,72 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return $this->masterNames;
}
// ------------------------------------------------------------------
// Setup Required Check
// ------------------------------------------------------------------
/**
* Check if the site has been provisioned for a new client and needs
* fresh setup information (company name, contact details).
*
* Shows a persistent admin banner until the setup flag is cleared
* by saving the core plugin settings.
*
* @return void
*
* @since 02.35.00
*/
protected function checkSetupRequired(): void
{
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
if (!file_exists($flagFile))
{
return;
}
$this->app->enqueueMessage(
'<strong>New client setup required.</strong> This site has been provisioned for a new client. '
. 'Please update the site name, contact details, and save the MokoWaaS plugin settings to complete setup. '
. '<a href="index.php?option=com_plugins&task=plugin.edit&extension_id='
. $this->getPluginExtensionId() . '" class="btn btn-sm btn-warning ms-2">Open Settings</a>',
'warning'
);
}
/**
* Get this plugin's extension_id.
*/
private function getPluginExtensionId(): int
{
static $id = null;
if ($id !== null)
{
return $id;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('extension_id'))
->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'))
);
$id = (int) $db->loadResult();
}
catch (\Throwable $e)
{
$id = 0;
}
return $id;
}
// ------------------------------------------------------------------
// One-Time Remote Login
// ------------------------------------------------------------------
@@ -2169,85 +2243,134 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{
$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') ?: [];
// Sync: copy any new download keys FROM Joomla's update_sites TO our table
$this->syncKeysToTable($db);
$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'
);
}
// Apply: re-apply all stored keys FROM our table TO Joomla's update_sites
$this->applyKeysFromTable($db);
}
catch (\Throwable $e)
{
// Non-critical — don't break the site over key backup
// Non-critical
}
}
/**
* Copy non-empty download keys from Joomla's update_sites to our persistent table.
*/
private function syncKeysToTable($db): void
{
// Find all update sites with download keys
$db->setQuery(
$db->getQuery(true)
->select([
'us.' . $db->quoteName('extra_query'),
'us.' . $db->quoteName('location'),
'e.' . $db->quoteName('element'),
])
->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('us.extra_query') . ' LIKE ' . $db->quote('%dlid=%'))
);
$rows = $db->loadObjectList() ?: [];
$now = gmdate('Y-m-d H:i:s');
foreach ($rows as $row)
{
parse_str($row->extra_query, $parsed);
$dlid = $parsed['dlid'] ?? '';
if (empty($dlid))
{
continue;
}
// Upsert into our table
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('element') . ' = ' . $db->quote($row->element))
);
if ((int) $db->loadResult() > 0)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_download_keys'))
->set($db->quoteName('dlid') . ' = ' . $db->quote($dlid))
->set($db->quoteName('location') . ' = ' . $db->quote($row->location))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('element') . ' = ' . $db->quote($row->element))
)->execute();
}
else
{
$db->setQuery(
$db->getQuery(true)
->insert($db->quoteName('#__mokowaas_download_keys'))
->columns([$db->quoteName('element'), $db->quoteName('location'), $db->quoteName('dlid'), $db->quoteName('created'), $db->quoteName('modified')])
->values(implode(',', [
$db->quote($row->element),
$db->quote($row->location),
$db->quote($dlid),
$db->quote($now),
$db->quote($now),
]))
)->execute();
}
}
}
/**
* Re-apply all stored download keys from our table to Joomla's update_sites.
*/
private function applyKeysFromTable($db): void
{
try
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('dlid') . ' != ' . $db->quote(''))
);
}
catch (\Throwable $e)
{
// Table might not exist yet (before install SQL runs)
return;
}
$keys = $db->loadObjectList() ?: [];
foreach ($keys as $key)
{
// Find update sites for this extension that are missing the key
$db->setQuery(
$db->getQuery(true)
->select('us.' . $db->quoteName('update_site_id'))
->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($key->element))
->where('(' . $db->quoteName('us.extra_query') . ' = ' . $db->quote('')
. ' OR ' . $db->quoteName('us.extra_query') . ' NOT LIKE ' . $db->quote('%dlid=%') . ')')
);
$siteIds = $db->loadColumn() ?: [];
foreach ($siteIds as $siteId)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $key->dlid))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
)->execute();
}
}
}
}
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.34.16
* VERSION: 02.34.26
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.16
* VERSION: 02.34.26
* PATH: /src/script.php
* BRIEF: Installation script for MokoWaaS plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.34.16
* VERSION: 02.34.26
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
@@ -109,16 +109,30 @@ class Monitor extends CMSPlugin implements SubscriberInterface
$app = $this->getApplication();
$config = Factory::getConfig();
$payload = [
'token' => $healthToken,
'domain' => $domain,
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
'site_url' => $siteUrl,
'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'token' => $healthToken,
'domain' => $domain,
'site_name' => $config->get('sitename', 'Joomla'),
'site_url' => $siteUrl,
'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'mokowaas_version' => $this->getMokoWaaSVersion(),
'client_info' => [
'company' => $config->get('sitename', ''),
'email' => $config->get('mailfrom', ''),
],
];
// Include live health data by calling the local health endpoint
$healthData = $this->fetchLocalHealth($siteUrl, $healthToken);
if ($healthData !== null)
{
$payload['health'] = $healthData;
}
$endpoint = $baseUrl . '/api/index.php/v1/mokowaasbase/heartbeat';
$json = json_encode($payload, JSON_UNESCAPED_SLASHES);
@@ -165,6 +179,42 @@ class Monitor extends CMSPlugin implements SubscriberInterface
}
}
/**
* 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
{
$url = $siteUrl . '/?mokowaas=health';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $healthToken,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || empty($response))
{
return null;
}
return json_decode($response, true) ?: null;
}
/**
* Get the installed MokoWaaS package version.
*/
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
* VERSION: 02.34.08
* VERSION: 02.34.26
* BRIEF: Content-only snapshot/restore for demo site reset
*/
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<description>PLG_TASK_MOKOWAASSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
* VERSION: 02.34.08
* VERSION: 02.34.26
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
* VERSION: 02.34.08
* VERSION: 02.34.26
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<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>
<files>
@@ -118,5 +118,11 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
'remotelogin',
['component' => 'com_mokowaas']
);
$router->createCRUDRoutes(
'v1/mokowaas/provision-reset',
'provision',
['component' => 'com_mokowaas']
);
}
}
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<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>
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /source/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.34.16
* VERSION: 02.34.26
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.34.16
* VERSION: 02.34.26
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.34.15</version>
<version>02.34.26-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+253
View File
@@ -39,8 +39,16 @@ class Pkg_MokowaasInstallerScript
* with no default, causing INSERT failures when Joomla's package installer
* creates placeholder rows before processing sub-extension manifests.
*/
/** @var array Download keys saved before Joomla wipes update sites */
private array $savedDownloadKeys = [];
public function preflight($type, $parent)
{
// CRITICAL: backup download keys BEFORE Joomla's installer wipes update sites.
// Joomla deletes and recreates #__update_sites rows from the manifest
// between preflight and postflight, clearing extra_query (dlid).
$this->savedDownloadKeys = $this->backupDownloadKeys();
try
{
$db = Factory::getDbo();
@@ -101,6 +109,10 @@ class Pkg_MokowaasInstallerScript
// Clean up stale/duplicate update sites
$this->cleanupStaleUpdateSites();
// Restore download keys: first from preflight backup, then from DB table
$this->restoreDownloadKeys($this->savedDownloadKeys);
$this->reapplyKeysFromDatabase();
// Fix orphaned update records (extension_id=0)
$this->fixUpdateRecords();
@@ -656,6 +668,247 @@ class Pkg_MokowaasInstallerScript
}
}
/**
* Backup all non-empty extra_query values from update sites.
*
* @return array Map of update_site_id => extra_query
*/
private function backupDownloadKeys(): array
{
$keys = [];
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query'), $db->quoteName('location')])
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('extra_query') . ' != ' . $db->quote(''))
);
$rows = $db->loadObjectList() ?: [];
foreach ($rows as $row)
{
$keys[$row->location] = $row->extra_query;
$keys['id_' . $row->update_site_id] = $row->extra_query;
}
// Also save to our persistent database table
$this->syncKeysToDatabase($db, $rows);
}
catch (\Throwable $e)
{
// Non-critical
}
return $keys;
}
/**
* Sync current download keys to the persistent #__mokowaas_download_keys table.
*/
private function syncKeysToDatabase($db, array $rows): void
{
try
{
// Check if table exists
$tables = $db->getTableList();
$prefix = $db->getPrefix();
if (!\in_array($prefix . 'mokowaas_download_keys', $tables, true))
{
return;
}
$now = gmdate('Y-m-d H:i:s');
foreach ($rows as $row)
{
parse_str($row->extra_query, $parsed);
$dlid = $parsed['dlid'] ?? '';
if (empty($dlid))
{
continue;
}
// Find the element for this update site
$db->setQuery(
$db->getQuery(true)
->select('e.' . $db->quoteName('element'))
->from($db->quoteName('#__update_sites_extensions', 'use'))
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('use.update_site_id') . ' = ' . (int) $row->update_site_id),
0, 1
);
$element = (string) $db->loadResult();
if (empty($element))
{
continue;
}
// Upsert
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
);
if ((int) $db->loadResult() > 0)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_download_keys'))
->set($db->quoteName('dlid') . ' = ' . $db->quote($dlid))
->set($db->quoteName('location') . ' = ' . $db->quote($row->location))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
)->execute();
}
else
{
$db->setQuery(
$db->getQuery(true)
->insert($db->quoteName('#__mokowaas_download_keys'))
->columns([$db->quoteName('element'), $db->quoteName('location'), $db->quoteName('dlid'), $db->quoteName('created'), $db->quoteName('modified')])
->values(implode(',', [
$db->quote($element), $db->quote($row->location), $db->quote($dlid), $db->quote($now), $db->quote($now),
]))
)->execute();
}
}
}
catch (\Throwable $e)
{
// Non-critical — table may not exist yet
}
}
/**
* Re-apply all download keys from our persistent database table.
*/
private function reapplyKeysFromDatabase(): void
{
try
{
$db = Factory::getDbo();
$tables = $db->getTableList();
$prefix = $db->getPrefix();
if (!\in_array($prefix . 'mokowaas_download_keys', $tables, true))
{
return;
}
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_download_keys'))
->where($db->quoteName('dlid') . ' != ' . $db->quote(''))
);
$keys = $db->loadObjectList() ?: [];
$restored = 0;
foreach ($keys as $key)
{
$db->setQuery(
$db->getQuery(true)
->select('us.' . $db->quoteName('update_site_id'))
->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($key->element))
->where('(' . $db->quoteName('us.extra_query') . ' = ' . $db->quote('')
. ' OR ' . $db->quoteName('us.extra_query') . ' NOT LIKE ' . $db->quote('%dlid=%') . ')')
);
$siteIds = $db->loadColumn() ?: [];
foreach ($siteIds as $siteId)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $key->dlid))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
)->execute();
$restored++;
}
}
if ($restored > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Re-applied %d download key(s) from persistent storage.', $restored),
'message'
);
}
}
catch (\Throwable $e)
{
// Non-critical
}
}
/**
* Restore download keys that were cleared by update site cleanup.
*
* @param array $savedKeys Map from backupDownloadKeys()
*/
private function restoreDownloadKeys(array $savedKeys): void
{
if (empty($savedKeys))
{
return;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query'), $db->quoteName('location')])
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('extra_query') . ' = ' . $db->quote(''))
);
$sites = $db->loadObjectList() ?: [];
$restored = 0;
foreach ($sites as $site)
{
// Try to match by location URL first, then by old ID
$key = $savedKeys[$site->location] ?? $savedKeys['id_' . $site->update_site_id] ?? '';
if (!empty($key))
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($key))
->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id)
)->execute();
$restored++;
}
}
if ($restored > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Restored %d download key(s) after update site cleanup.', $restored),
'message'
);
}
}
catch (\Throwable $e)
{
// Non-critical
}
}
/**
* Ensure the MokoWaaS update server entry stays enabled and points
* to the correct dynamic endpoint with the license key attached.
-45
View File
@@ -1,45 +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
VERSION: 02.34.16-dev
-->
<updates>
<update>
<name>Package - MokoWaaS</name>
<description>Package - MokoWaaS development build.</description>
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.34.16-dev</version>
<creationDate>2026-06-06</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.34.16-dev.zip</downloadurl>
</downloads>
<tags><tag>dev</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update>
<update>
<name>Package - MokoWaaS</name>
<description>Package - MokoWaaS: admin dashboard, security firewall, helpdesk, privacy guard, database tools, and more.</description>
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.33.00</version>
<creationDate>2026-06-04</creationDate>
<infourl title="Package - MokoWaaS">https://mokoconsulting.tech/products/mokowaas</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.33.00.zip</downloadurl>
</downloads>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>