diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 11958bdc..45b504ba 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.12.06 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ed2a44..86a037a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ - **Analytics service filter**: Filter heatmap and stats by service type with configurable date range - **Analytics service breakdown**: Per-service success rate, failure count, and average posts per day - **Analytics AJAX endpoint**: JSON heatmap data for dynamic filtering without page reload -- **Social image generator**: Generate Open Graph images with article title overlay using PHP GD library (#157) -- **Social image config**: Background color, text color, overlay style, and site name override in component options (#157) +- **Social image generator**: Generate branded 1200x630 OG images with article title overlay using PHP GD (#157) +- **Social image config**: Background color, text color, font size, and site name branding options (#157) +- **Generate Social Image button**: One-click image generation in the Share Content panel (#157) - **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161) - **AI provider config**: New "AI Caption Generation" fieldset in component options with provider, API key, model, and tone settings - **AI Generate button**: One-click AI generation button in the Share Content panel that fills all caption fields diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 193d7ad5..0b138bf6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ - VERSION: 01.12.00 + VERSION: 01.12.06 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index eb4252c6..b63745a6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.Template-Joomla INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/Template-Joomla - VERSION: 01.12.00 + VERSION: 01.12.06 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for Template-Joomla --> diff --git a/README.md b/README.md index 64995ac0..be11b24e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 6. diff --git a/SECURITY.md b/SECURITY.md index bb261b18..d6f34957 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 01.12.00 +VERSION: 01.12.06 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitecross/config.xml b/source/packages/com_mokosuitecross/config.xml index c93837fd..8385117d 100644 --- a/source/packages/com_mokosuitecross/config.xml +++ b/source/packages/com_mokosuitecross/config.xml @@ -266,7 +266,7 @@ -
+
com_mokosuitecross - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitecross/sql/install.mysql.sql b/source/packages/com_mokosuitecross/sql/install.mysql.sql index 0a788f58..e40c8a08 100644 --- a/source/packages/com_mokosuitecross/sql/install.mysql.sql +++ b/source/packages/com_mokosuitecross/sql/install.mysql.sql @@ -96,6 +96,27 @@ INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_bo ('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()), ('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW()); + +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_analytics` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `post_id` int unsigned NOT NULL, + `service_id` int unsigned NOT NULL, + `service_type` varchar(50) NOT NULL DEFAULT '', + `posted_at` datetime DEFAULT NULL, + `day_of_week` tinyint unsigned NOT NULL DEFAULT 0, + `hour_of_day` tinyint unsigned NOT NULL DEFAULT 0, + `impressions` int unsigned NOT NULL DEFAULT 0, + `engagements` int unsigned NOT NULL DEFAULT 0, + `clicks` int unsigned NOT NULL DEFAULT 0, + `shares` int unsigned NOT NULL DEFAULT 0, + `engagement_rate` decimal(5,2) NOT NULL DEFAULT 0.00, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_service_type` (`service_type`), + KEY `idx_day_hour` (`day_of_week`, `hour_of_day`), + KEY `idx_post` (`post_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `category_id` int(10) unsigned NOT NULL, diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.54.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.54.sql index ac182a97..8cba2fa6 100644 --- a/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.54.sql +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.54.sql @@ -1 +1,23 @@ -/* 01.08.54 — no schema changes */ +-- MokoSuiteCross 01.08.54 -- Best time to post analytics +-- Copyright (C) 2026 Moko Consulting. All rights reserved. +-- SPDX-License-Identifier: GPL-3.0-or-later + +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_analytics` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `post_id` int unsigned NOT NULL, + `service_id` int unsigned NOT NULL, + `service_type` varchar(50) NOT NULL DEFAULT '', + `posted_at` datetime DEFAULT NULL, + `day_of_week` tinyint unsigned NOT NULL DEFAULT 0, + `hour_of_day` tinyint unsigned NOT NULL DEFAULT 0, + `impressions` int unsigned NOT NULL DEFAULT 0, + `engagements` int unsigned NOT NULL DEFAULT 0, + `clicks` int unsigned NOT NULL DEFAULT 0, + `shares` int unsigned NOT NULL DEFAULT 0, + `engagement_rate` decimal(5,2) NOT NULL DEFAULT 0.00, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_service_type` (`service_type`), + KEY `idx_day_hour` (`day_of_week`, `hour_of_day`), + KEY `idx_post` (`post_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.59.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.59.sql new file mode 100644 index 00000000..bdb022b9 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.59.sql @@ -0,0 +1 @@ +/* 01.08.59 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.01.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.01.sql new file mode 100644 index 00000000..3ceaf65e --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.01.sql @@ -0,0 +1 @@ +/* 01.12.01 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.03.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.03.sql new file mode 100644 index 00000000..910c11c4 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.03.sql @@ -0,0 +1 @@ +/* 01.12.03 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.04.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.04.sql new file mode 100644 index 00000000..fcf5592a --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.04.sql @@ -0,0 +1 @@ +/* 01.12.04 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.05.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.05.sql new file mode 100644 index 00000000..1d4d5a69 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.05.sql @@ -0,0 +1 @@ +/* 01.12.05 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.06.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.06.sql new file mode 100644 index 00000000..60f87db8 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.06.sql @@ -0,0 +1 @@ +/* 01.12.06 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php b/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php index fce9f59b..d7eb2e1e 100644 --- a/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php +++ b/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php @@ -14,11 +14,84 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Session\Session; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\AnalyticsHelper; class AnalyticsController extends BaseController { - public function display($cachable = false, $urlparams = []): static + /** + * Return heatmap grid data as JSON. + * + * Query params: service_type (string), days (int, default 90) + */ + public function heatmap(): void { - return parent::display($cachable, $urlparams); + if (!Session::checkToken('get')) { + echo json_encode(['success' => false, 'error' => 'Invalid token']); + $this->app->close(); + + return; + } + + $user = $this->app->getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokosuitecross')) { + echo json_encode(['success' => false, 'error' => 'Permission denied']); + $this->app->close(); + + return; + } + + $serviceType = $this->input->getCmd('service_type', ''); + $days = $this->input->getInt('days', 90); + + $grid = AnalyticsHelper::getHeatmapData($serviceType, $days); + $bestTimes = AnalyticsHelper::getBestTimes($serviceType, 3); + + $this->app->setHeader('Content-Type', 'application/json; charset=utf-8'); + echo json_encode([ + 'success' => true, + 'grid' => $grid, + 'best_times' => $bestTimes, + ]); + $this->app->close(); + } + + /** + * Return the top posting times as JSON. + * + * Query params: service_type (string), limit (int, default 5) + */ + public function besttimes(): void + { + if (!Session::checkToken('get')) { + echo json_encode(['success' => false, 'error' => 'Invalid token']); + $this->app->close(); + + return; + } + + $user = $this->app->getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokosuitecross')) { + echo json_encode(['success' => false, 'error' => 'Permission denied']); + $this->app->close(); + + return; + } + + $serviceType = $this->input->getCmd('service_type', ''); + $limit = $this->input->getInt('limit', 5); + + $bestTimes = AnalyticsHelper::getBestTimes($serviceType, $limit); + $serviceBreakdown = AnalyticsHelper::getServiceBreakdown(); + + $this->app->setHeader('Content-Type', 'application/json; charset=utf-8'); + echo json_encode([ + 'success' => true, + 'best_times' => $bestTimes, + 'service_breakdown' => $serviceBreakdown, + ]); + $this->app->close(); } } diff --git a/source/packages/com_mokosuitecross/src/Controller/CalendarController.php b/source/packages/com_mokosuitecross/src/Controller/CalendarController.php index e36b117e..3e88763e 100644 --- a/source/packages/com_mokosuitecross/src/Controller/CalendarController.php +++ b/source/packages/com_mokosuitecross/src/Controller/CalendarController.php @@ -13,12 +13,256 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Session\Session; +use Joomla\Component\MokoSuiteCross\Administrator\Table\PostTable; +/** + * Calendar controller -- provides AJAX endpoints for the visual post calendar. + * + * Endpoints: + * task=calendar.events -- GET JSON feed for FullCalendar (filtered by start/end) + * task=calendar.reschedule -- POST reschedule a post to a new date/time + */ class CalendarController extends BaseController { - public function display($cachable = false, $urlparams = []): static + /** + * Return posts as FullCalendar-compatible JSON events. + * + * Query params: start, end (ISO 8601 date range from FullCalendar). + * + * @return void + */ + public function events(): void { - return parent::display($cachable, $urlparams); + $app = $this->app; + $db = Factory::getDbo(); + + // ACL check + if (!$app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { + $this->sendJsonResponse(['error' => 'Forbidden'], 403); + + return; + } + + // FullCalendar sends start/end as ISO date strings + $start = $this->input->getString('start', ''); + $end = $this->input->getString('end', ''); + + $query = $db->getQuery(true) + ->select([ + 'p.' . $db->quoteName('id'), + 'p.' . $db->quoteName('article_id'), + 'p.' . $db->quoteName('service_id'), + 'p.' . $db->quoteName('status'), + 'p.' . $db->quoteName('scheduled_at'), + 'p.' . $db->quoteName('posted_at'), + 'p.' . $db->quoteName('created'), + 'p.' . $db->quoteName('message'), + 'a.' . $db->quoteName('title', 'article_title'), + 's.' . $db->quoteName('title', 'service_title'), + 's.' . $db->quoteName('service_type'), + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->leftJoin( + $db->quoteName('#__content', 'a') + . ' ON ' . $db->quoteName('a.id') . ' = ' . $db->quoteName('p.article_id') + ) + ->leftJoin( + $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id') + ) + ->order($db->quoteName('p.created') . ' DESC'); + + // Filter by date range when provided + if ($start !== '') { + $dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)'; + $query->where($dateExpr . ' >= ' . $db->quote($start)); + } + + if ($end !== '') { + $dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)'; + $query->where($dateExpr . ' <= ' . $db->quote($end)); + } + + $db->setQuery($query); + $rows = $db->loadObjectList() ?: []; + + // Map status to colour + $statusColors = [ + 'posted' => '#28a745', + 'scheduled' => '#007bff', + 'queued' => '#ffc107', + 'failed' => '#dc3545', + 'posting' => '#17a2b8', + ]; + + $events = []; + + foreach ($rows as $row) { + // Pick the best date for the calendar event + $eventDate = $row->scheduled_at ?: ($row->posted_at ?: $row->created); + + // Skip rows with no usable date + if (empty($eventDate) || $eventDate === '0000-00-00 00:00:00') { + continue; + } + + $title = ($row->article_title ?: 'Post #' . $row->id); + + if ($row->service_title) { + $title .= ' - ' . $row->service_title; + } + + $events[] = [ + 'id' => (int) $row->id, + 'title' => $title, + 'start' => $eventDate, + 'color' => $statusColors[$row->status] ?? '#6c757d', + 'url' => 'index.php?option=com_mokosuitecross&task=post.edit&id=' . (int) $row->id, + 'extendedProps' => [ + 'status' => $row->status, + 'service_type' => $row->service_type ?? '', + 'article_id' => (int) $row->article_id, + 'service_id' => (int) $row->service_id, + 'message' => mb_substr($row->message ?? '', 0, 200), + ], + ]; + } + + $this->sendJsonResponse($events, 200); + } + + /** + * Reschedule a post to a new date/time via drag-drop. + * + * POST params: post_id (int), new_date (ISO 8601 datetime string). + * + * @return void + */ + public function reschedule(): void + { + $app = $this->app; + + // CSRF check + if (!Session::checkToken('post')) { + $this->sendJsonResponse(['error' => Text::_('JINVALID_TOKEN')], 403); + + return; + } + + // ACL check + if (!$app->getIdentity()->authorise('core.edit', 'com_mokosuitecross')) { + $this->sendJsonResponse(['error' => 'Forbidden'], 403); + + return; + } + + $postId = $this->input->getInt('post_id', 0); + $newDate = $this->input->getString('new_date', ''); + + if ($postId < 1 || $newDate === '') { + $this->sendJsonResponse( + ['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')], + 400 + ); + + return; + } + + // Validate the date format + try { + $dateObj = Factory::getDate($newDate); + } catch (\Exception $e) { + $this->sendJsonResponse( + ['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')], + 400 + ); + + return; + } + + // Load the post using Table bind/check/store pattern + $db = Factory::getDbo(); + $table = new PostTable($db); + + if (!$table->load($postId)) { + $this->sendJsonResponse( + ['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')], + 404 + ); + + return; + } + + // Only allow rescheduling of scheduled or queued posts + $allowedStatuses = ['scheduled', 'queued']; + + if (!in_array($table->status, $allowedStatuses, true)) { + $this->sendJsonResponse( + ['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')], + 400 + ); + + return; + } + + // Update the post + $data = [ + 'scheduled_at' => $dateObj->toSql(), + 'status' => 'scheduled', + 'modified' => Factory::getDate()->toSql(), + ]; + + if (!$table->bind($data) || !$table->check() || !$table->store()) { + $this->sendJsonResponse( + ['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')], + 500 + ); + + return; + } + + // Log the reschedule + $log = (object) [ + 'post_id' => $postId, + 'service_id' => (int) $table->service_id, + 'level' => 'info', + 'message' => sprintf('Post rescheduled to %s via calendar drag-drop', $dateObj->toSql()), + 'context' => '{}', + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitecross_logs', $log); + + $this->sendJsonResponse( + [ + 'success' => true, + 'message' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS'), + ], + 200 + ); + } + + /** + * Send a JSON response and close the application. + * + * @param array $data Response data + * @param int $httpCode HTTP status code + * + * @return void + */ + private function sendJsonResponse(array $data, int $httpCode): void + { + $app = $this->app; + + $app->setHeader('Content-Type', 'application/json; charset=utf-8'); + $app->setHeader('Status', (string) $httpCode); + + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + $app->close(); } } diff --git a/source/packages/com_mokosuitecross/src/Controller/SocialImageController.php b/source/packages/com_mokosuitecross/src/Controller/SocialImageController.php index 36462869..c3010299 100644 --- a/source/packages/com_mokosuitecross/src/Controller/SocialImageController.php +++ b/source/packages/com_mokosuitecross/src/Controller/SocialImageController.php @@ -17,7 +17,6 @@ use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Session\Session; -use Joomla\CMS\Uri\Uri; use Joomla\Component\MokoSuiteCross\Administrator\Helper\SocialImageHelper; class SocialImageController extends BaseController @@ -33,7 +32,7 @@ class SocialImageController extends BaseController $user = $this->app->getIdentity(); - if (!$user->authorise('core.manage', 'com_mokosuitecross')) { + if (!$user->authorise('core.edit', 'com_mokosuitecross')) { echo json_encode(['success' => false, 'error' => 'Permission denied']); $this->app->close(); @@ -49,47 +48,40 @@ class SocialImageController extends BaseController return; } + $params = ComponentHelper::getParams('com_mokosuitecross'); + + if (!(int) $params->get('social_image_enabled', 0)) { + echo json_encode(['success' => false, 'error' => 'Social image generator is not enabled']); + $this->app->close(); + + return; + } + $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'title', 'images'])) + ->select($db->quoteName('title')) ->from($db->quoteName('#__content')) ->where($db->quoteName('id') . ' = ' . $articleId); $db->setQuery($query); - $article = $db->loadObject(); + $title = $db->loadResult(); - if (!$article) { + if (!$title) { echo json_encode(['success' => false, 'error' => 'Article not found']); $this->app->close(); return; } - $params = ComponentHelper::getParams('com_mokosuitecross'); - $siteName = $params->get('social_image_site_name', '') ?: Factory::getApplication()->get('sitename', ''); + $siteName = $this->app->get('sitename', ''); - $options = [ - 'bg_color' => $params->get('social_image_bg_color', '#1a1a2e'), - 'text_color' => $params->get('social_image_text_color', '#ffffff'), - 'overlay' => $params->get('social_image_overlay', 'dark'), + $config = [ + 'bg_color' => $params->get('social_image_bg_color', '#1a1a2e'), + 'text_color' => $params->get('social_image_text_color', '#ffffff'), + 'font_size' => $params->get('social_image_font_size', 48), + 'show_site_name' => (bool) $params->get('social_image_show_site_name', 1), ]; - $backgroundPath = null; - $images = json_decode($article->images ?? '{}', true); - - if (!empty($images['image_intro'])) { - $backgroundPath = JPATH_ROOT . '/' . ltrim($images['image_intro'], '/'); - } elseif (!empty($images['image_fulltext'])) { - $backgroundPath = JPATH_ROOT . '/' . ltrim($images['image_fulltext'], '/'); - } - - try { - $imagePath = SocialImageHelper::generate($article->title, $siteName, $backgroundPath, $options); - $imageUrl = str_replace(JPATH_ROOT, Uri::root(true), str_replace('\\', '/', $imagePath)); - - $result = ['success' => true, 'image_url' => $imageUrl, 'image_path' => $imagePath]; - } catch (\Throwable $e) { - $result = ['success' => false, 'error' => $e->getMessage()]; - } + $result = SocialImageHelper::generate($title, $siteName, $config); $this->app->setHeader('Content-Type', 'application/json; charset=utf-8'); echo json_encode($result); diff --git a/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php index a2b324ff..47e917a9 100644 --- a/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php +++ b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php @@ -17,144 +17,236 @@ use Joomla\CMS\Factory; class AnalyticsHelper { - private static array $dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + /** + * Record or update engagement metrics for a post. + * + * @param int $postId The post ID + * @param int $serviceId The service ID + * @param string $serviceType The service type (e.g. twitter, facebook) + * @param array $metrics Engagement metrics: impressions, engagements, clicks, shares, posted_at + * + * @return bool True on success + */ + public static function recordEngagement(int $postId, int $serviceId, string $serviceType, array $metrics): bool + { + $db = Factory::getDbo(); - public static function getPostingHeatmap(string $serviceType = '', int $days = 90): array + $postedAt = $metrics['posted_at'] ?? null; + + if ($postedAt) { + $timestamp = strtotime($postedAt); + $dayOfWeek = (int) date('w', $timestamp); + $hourOfDay = (int) date('G', $timestamp); + } else { + $dayOfWeek = 0; + $hourOfDay = 0; + } + + $impressions = (int) ($metrics['impressions'] ?? 0); + $engagements = (int) ($metrics['engagements'] ?? 0); + $clicks = (int) ($metrics['clicks'] ?? 0); + $shares = (int) ($metrics['shares'] ?? 0); + + $engagementRate = $impressions > 0 + ? round(($engagements / $impressions) * 100, 2) + : 0.00; + + // Check if a row already exists for this post + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__mokosuitecross_analytics')) + ->where($db->quoteName('post_id') . ' = ' . $postId) + ->where($db->quoteName('service_id') . ' = ' . $serviceId); + $db->setQuery($query); + $existingId = $db->loadResult(); + + if ($existingId) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_analytics')) + ->set($db->quoteName('impressions') . ' = ' . $impressions) + ->set($db->quoteName('engagements') . ' = ' . $engagements) + ->set($db->quoteName('clicks') . ' = ' . $clicks) + ->set($db->quoteName('shares') . ' = ' . $shares) + ->set($db->quoteName('engagement_rate') . ' = ' . $engagementRate) + ->where($db->quoteName('id') . ' = ' . (int) $existingId); + $db->setQuery($query); + $db->execute(); + + return true; + } + + $record = (object) [ + 'post_id' => $postId, + 'service_id' => $serviceId, + 'service_type' => $serviceType, + 'posted_at' => $postedAt, + 'day_of_week' => $dayOfWeek, + 'hour_of_day' => $hourOfDay, + 'impressions' => $impressions, + 'engagements' => $engagements, + 'clicks' => $clicks, + 'shares' => $shares, + 'engagement_rate' => $engagementRate, + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitecross_analytics', $record); + + return true; + } + + /** + * Get heatmap data as a 7x24 grid of average engagement rates. + * + * @param string $serviceType Optional service type filter + * @param int $days Number of days to look back (0 = all time) + * + * @return array 7x24 grid: [ day_of_week => [ hour_of_day => avg_engagement_rate ] ] + */ + public static function getHeatmapData(string $serviceType = '', int $days = 90): array { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select('DAYOFWEEK(' . $db->quoteName('p.posted_at') . ') - 1 AS dow') - ->select('HOUR(' . $db->quoteName('p.posted_at') . ') AS hr') - ->select('COUNT(*) AS cnt') - ->from($db->quoteName('#__mokosuitecross_posts', 'p')) - ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) - ->where($db->quoteName('p.posted_at') . ' IS NOT NULL'); - - if ($days > 0) { - $since = Factory::getDate('now - ' . $days . ' days')->toSql(); - $query->where($db->quoteName('p.posted_at') . ' >= ' . $db->quote($since)); - } + ->select([ + $db->quoteName('day_of_week'), + $db->quoteName('hour_of_day'), + 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_rate', + 'COUNT(*) AS post_count', + ]) + ->from($db->quoteName('#__mokosuitecross_analytics')) + ->group($db->quoteName('day_of_week')) + ->group($db->quoteName('hour_of_day')) + ->order($db->quoteName('day_of_week') . ' ASC') + ->order($db->quoteName('hour_of_day') . ' ASC'); if ($serviceType !== '') { - $query->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) - ->where($db->quoteName('s.service_type') . ' = ' . $db->quote($serviceType)); + $query->where($db->quoteName('service_type') . ' = ' . $db->quote($serviceType)); } - $query->group('dow, hr') - ->order('dow ASC, hr ASC'); + if ($days > 0) { + $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); + $query->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff)); + } $db->setQuery($query); $rows = $db->loadObjectList(); + // Build 7x24 grid initialised to zero $grid = []; for ($d = 0; $d < 7; $d++) { - $grid[$d] = array_fill(0, 24, 0); + for ($h = 0; $h < 24; $h++) { + $grid[$d][$h] = ['avg_rate' => 0.00, 'post_count' => 0]; + } } foreach ($rows as $row) { - $grid[(int) $row->dow][(int) $row->hr] = (int) $row->cnt; + $grid[(int) $row->day_of_week][(int) $row->hour_of_day] = [ + 'avg_rate' => round((float) $row->avg_rate, 2), + 'post_count' => (int) $row->post_count, + ]; } return $grid; } - public static function getBestTimes(string $serviceType = '', int $days = 90, int $limit = 5): array + /** + * Get the best times to post ranked by average engagement rate. + * + * @param string $serviceType Optional service type filter + * @param int $limit Number of results to return + * + * @return array List of [day_of_week, hour_of_day, avg_rate, post_count] + */ + public static function getBestTimes(string $serviceType = '', int $limit = 5): array { - $grid = self::getPostingHeatmap($serviceType, $days); - $slots = []; + $db = Factory::getDbo(); - foreach ($grid as $dow => $hours) { - foreach ($hours as $hour => $count) { - if ($count > 0) { - $slots[] = [ - 'day' => self::$dayNames[$dow], - 'hour' => $hour, - 'count' => $count, - 'label' => self::$dayNames[$dow] . ' ' . self::formatHour($hour), - ]; - } - } + $query = $db->getQuery(true) + ->select([ + $db->quoteName('day_of_week'), + $db->quoteName('hour_of_day'), + 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_rate', + 'COUNT(*) AS post_count', + ]) + ->from($db->quoteName('#__mokosuitecross_analytics')) + ->group($db->quoteName('day_of_week')) + ->group($db->quoteName('hour_of_day')) + ->having('COUNT(*) >= 1') + ->order('avg_rate DESC'); + + if ($serviceType !== '') { + $query->where($db->quoteName('service_type') . ' = ' . $db->quote($serviceType)); } - usort($slots, static fn($a, $b) => $b['count'] <=> $a['count']); + $db->setQuery($query, 0, $limit); + $rows = $db->loadAssocList(); - return \array_slice($slots, 0, $limit); + $dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + + $results = []; + + foreach ($rows as $row) { + $hour = (int) $row['hour_of_day']; + $ampm = $hour < 12 ? 'AM' : 'PM'; + $hour12 = $hour % 12 ?: 12; + + $results[] = [ + 'day_of_week' => (int) $row['day_of_week'], + 'day_name' => $dayNames[(int) $row['day_of_week']], + 'hour_of_day' => $hour, + 'hour_label' => $hour12 . ':00 ' . $ampm, + 'avg_rate' => round((float) $row['avg_rate'], 2), + 'post_count' => (int) $row['post_count'], + ]; + } + + return $results; } + /** + * Get engagement stats grouped by service type. + * + * @param int $days Number of days to look back (0 = all time) + * + * @return array List of [service_type, total_posts, avg_engagement_rate, total_impressions, total_engagements] + */ public static function getServiceBreakdown(int $days = 30): array { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select($db->quoteName('s.service_type')) - ->select($db->quoteName('s.title', 'service_title')) - ->select('COUNT(*) AS total') - ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success') - ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed') - ->from($db->quoteName('#__mokosuitecross_posts', 'p')) - ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')); + ->select([ + $db->quoteName('service_type'), + 'COUNT(*) AS total_posts', + 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_engagement_rate', + 'SUM(' . $db->quoteName('impressions') . ') AS total_impressions', + 'SUM(' . $db->quoteName('engagements') . ') AS total_engagements', + 'SUM(' . $db->quoteName('clicks') . ') AS total_clicks', + 'SUM(' . $db->quoteName('shares') . ') AS total_shares', + ]) + ->from($db->quoteName('#__mokosuitecross_analytics')) + ->group($db->quoteName('service_type')) + ->order('avg_engagement_rate DESC'); if ($days > 0) { - $since = Factory::getDate('now - ' . $days . ' days')->toSql(); - $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since)); + $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); + $query->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff)); } - $query->group($db->quoteName(['s.service_type', 's.title'])) - ->order('total DESC'); - $db->setQuery($query); - $rows = $db->loadObjectList(); + $rows = $db->loadAssocList(); - $result = []; - - foreach ($rows as $row) { - $total = (int) $row->total; - $success = (int) $row->success; - $result[] = [ - 'service_type' => $row->service_type, - 'service_title' => $row->service_title, - 'total' => $total, - 'success' => $success, - 'failed' => (int) $row->failed, - 'success_rate' => $total > 0 ? round(($success / $total) * 100, 1) : 0.0, - 'avg_per_day' => $days > 0 ? round($total / $days, 1) : 0.0, - ]; + foreach ($rows as &$row) { + $row['avg_engagement_rate'] = round((float) $row['avg_engagement_rate'], 2); + $row['total_posts'] = (int) $row['total_posts']; + $row['total_impressions'] = (int) $row['total_impressions']; + $row['total_engagements'] = (int) $row['total_engagements']; + $row['total_clicks'] = (int) $row['total_clicks']; + $row['total_shares'] = (int) $row['total_shares']; } - return $result; + return $rows; } - - public static function getServiceTypes(): array - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('service_type')) - ->from($db->quoteName('#__mokosuitecross_services')) - ->where($db->quoteName('published') . ' = 1') - ->order($db->quoteName('service_type') . ' ASC'); - - $db->setQuery($query); - - return $db->loadColumn() ?: []; - } - - private static function formatHour(int $hour): string - { - if ($hour === 0) { - return '12:00 AM'; - } - - if ($hour < 12) { - return $hour . ':00 AM'; - } - - if ($hour === 12) { - return '12:00 PM'; - } - - return ($hour - 12) . ':00 PM'; - } -} +} \ No newline at end of file diff --git a/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php index 84b66d4d..e9bce359 100644 --- a/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php +++ b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php @@ -40,6 +40,7 @@ class MokoSuiteCrossHelper 'posts' => 'COM_MOKOSUITECROSS_SUBMENU_POSTS', 'services' => 'COM_MOKOSUITECROSS_SUBMENU_SERVICES', 'templates' => 'COM_MOKOSUITECROSS_SUBMENU_TEMPLATES', + 'calendar' => 'COM_MOKOSUITECROSS_SUBMENU_CALENDAR', 'logs' => 'COM_MOKOSUITECROSS_SUBMENU_LOGS', 'calendar' => 'COM_MOKOSUITECROSS_SUBMENU_CALENDAR', 'analytics' => 'COM_MOKOSUITECROSS_SUBMENU_ANALYTICS', diff --git a/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php index 58706228..caacba03 100644 --- a/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php +++ b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php @@ -14,52 +14,48 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\View\Calendar; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; class HtmlView extends BaseHtmlView { - public int $year; - public int $month; - public array $events; public $sidebar; + public $ajaxUrl; public function display($tpl = null): void { - $input = Factory::getApplication()->input; + // ACL check + $canDo = MokoSuiteCrossHelper::getActions(); - $this->year = $input->getInt('year', (int) date('Y')); - $this->month = $input->getInt('month', (int) date('n')); - - if ($this->month < 1 || $this->month > 12) { - $this->month = (int) date('n'); + if (!$canDo->get('core.manage')) { + throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); } - if ($this->year < 2000 || $this->year > 2100) { - $this->year = (int) date('Y'); - } - - $model = $this->getModel(); - $this->events = $model->getEvents($this->year, $this->month); + // Build AJAX URL for FullCalendar event source + $this->ajaxUrl = Route::_('index.php?option=com_mokosuitecross&task=calendar.events&format=json', false); $this->addToolbar(); MokoSuiteCrossHelper::addSubmenu('calendar'); $this->sidebar = \Joomla\CMS\HTML\Sidebar::render(); + // Set document title + Factory::getApplication()->getDocument()->setTitle( + Text::_('COM_MOKOSUITECROSS_CALENDAR') . ' - ' . Text::_('COM_MOKOSUITECROSS') + ); + parent::display($tpl); } protected function addToolbar(): void { - $canDo = MokoSuiteCrossHelper::getActions(); - - ToolbarHelper::title('MokoSuiteCross -- Post Calendar', 'calendar'); - ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitecross&view=dashboard'); - - if ($canDo->get('core.admin')) { - ToolbarHelper::preferences('com_mokosuitecross'); - } + ToolbarHelper::title( + Text::_('COM_MOKOSUITECROSS') . ' - ' . Text::_('COM_MOKOSUITECROSS_CALENDAR'), + 'calendar' + ); + ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_mokosuitecross&view=dashboard', false)); } } diff --git a/source/packages/com_mokosuitecross/tmpl/calendar/default.php b/source/packages/com_mokosuitecross/tmpl/calendar/default.php index dc0e2187..240484fb 100644 --- a/source/packages/com_mokosuitecross/tmpl/calendar/default.php +++ b/source/packages/com_mokosuitecross/tmpl/calendar/default.php @@ -12,118 +12,150 @@ defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Calendar\HtmlView $this */ -$year = $this->year; -$month = $this->month; -$events = $this->events; -$today = date('Y-m-d'); - -$prevMonth = $month - 1; -$prevYear = $year; - -if ($prevMonth < 1) { - $prevMonth = 12; - $prevYear--; -} - -$nextMonth = $month + 1; -$nextYear = $year; - -if ($nextMonth > 12) { - $nextMonth = 1; - $nextYear++; -} - -$monthName = date('F', mktime(0, 0, 0, $month, 1, $year)); -$daysInMonth = (int) date('t', mktime(0, 0, 0, $month, 1, $year)); -$firstWeekday = ((int) date('N', mktime(0, 0, 0, $month, 1, $year))) - 1; - -$statusClass = static function (string $status): string { - return match ($status) { - 'posted' => 'bg-success', - 'failed' => 'bg-danger', - default => 'bg-warning text-dark', - }; -}; +$token = Session::getFormToken(); +$ajaxUrl = $this->ajaxUrl; ?> -
- - - - -

- - - - + + +
+ + + +
-
- - - - - - - - - - - - - - - while ($day <= $daysInMonth) : ?> - - - - + diff --git a/source/packages/com_mokosuitecross/tmpl/dashboard/default.php b/source/packages/com_mokosuitecross/tmpl/dashboard/default.php index 4ac74e7c..b5289951 100644 --- a/source/packages/com_mokosuitecross/tmpl/dashboard/default.php +++ b/source/packages/com_mokosuitecross/tmpl/dashboard/default.php @@ -282,9 +282,9 @@ $queueProcessing = $componentParams->get('queue_processing', 'scheduler'); class="list-group-item list-group-item-action"> - - + diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index a797d4ba..041b96fc 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php index 3b3c7e28..99fe1bb1 100644 --- a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php +++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php @@ -257,6 +257,53 @@ XML; $form->load($aiXml); $form->setFieldAttribute('mokosuitecross_ai_generate', 'description', $aiButtonHtml, 'attribs'); } + // Social Image Generator button (#157) + $siParams = ComponentHelper::getParams('com_mokosuitecross'); + $siEnabled = (bool) $siParams->get('social_image_enabled', 0); + + if ($siEnabled && $articleId > 0) { + $siToken = Session::getFormToken(); + $siUrl = Uri::base() . 'index.php?option=com_mokosuitecross&task=socialimage.generate&format=raw&article_id=' . $articleId . '&' . $siToken . '=1'; + + $siButtonHtml = '
' + . '' + . '' + . '' + . '
' + . ''; + + $siXml = ' +
+ +
'; + $form->load($siXml); + $form->setFieldAttribute('mokosuitecross_si_generate', 'description', $siButtonHtml, 'attribs'); + } // Cross-post history panel for existing articles diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index e7c45c5e..e7719b2d 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index 6c7cac51..45ccdf0e 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index e48c48ab..508a2830 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index 7cb3cfe1..1cc70890 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index 12e964a8..907fd45f 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index c0a9ff6a..84d782b7 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index 3ab19bcc..a652514c 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index a7c9862a..9df9f2b7 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 2a249503..438e1310 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 3dfaebca..6529df4b 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index 0d7e9ecb..9fb1041e 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index 7aa1bae9..3310cf2d 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index d474b7ee..09bc269d 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml index 42e505b0..decfe385 100644 --- a/source/packages/plg_mokosuitecross_instagram/instagram.xml +++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Instagram - 01.12.00 + 01.12.06 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index d8f66e2a..6d02d4b5 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index bb728cc6..9c03c94b 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 3affc8a9..0df0dc1d 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index e4f8cefb..a6440cc2 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index 5ee00266..423d2256 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index 8e57fc9a..ac3a5648 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index ae9c5d2d..53345c03 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index d2d1e0a6..7112ca47 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index ba15ecaf..7ac4d4e6 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index d103a6ae..c1d4b67b 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index 42d35f37..29de5059 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index a0d8c930..fb62c347 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index 2e1c408c..b88b5d0c 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index df5b9c14..be2ad516 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index a3ba3569..057e3246 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 58414c7f..4af6dadd 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index e5d0e7f3..3384d217 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index 9dd7a5f1..29e85c4f 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index b7d14cea..08169c9a 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 8c87b722..0820978b 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index fdf655d1..78958ed7 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index d260ebcc..445ef3c8 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index 8184507d..e6e51a90 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml index 10537c39..20de935b 100644 --- a/source/packages/plg_mokosuitecross_youtube/youtube.xml +++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Youtube - 01.12.00 + 01.12.06 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index 7401b98c..3efd8c89 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index 11767a92..71c04013 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index a017db59..8accd397 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 96473d2c..51ae0a89 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index ae69ef00..78a7711c 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index 172bf5ef..9893f414 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.12.00 + 01.12.06 2026-05-28 Moko Consulting hello@mokoconsulting.tech