diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 3a22db58..a600541c 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
-# VERSION: 02.58.14
+# VERSION: 02.58.15
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f41d813c..94209868 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
- The backend controller, Web Services API controller, and legacy `cli/mokosuitebackup.php` now run backups through the shared `BackupRunner` (gaining the normalized complete/warning/fail status) instead of instantiating `BackupEngine` directly.
### Fixed
+- Package installer is now **honest about success**: the postflight no longer prints "installed successfully" / next-steps when the install actually failed or only partially completed. Joomla logs a failed child extension but still runs the package postflight, so the postflight now (a) verifies every bundled child declared in the manifest actually registered in `#__extensions` (matched by element + type, and folder/group for plugins) and (b) verifies the component's schema tables — derived dynamically from the installed `install.mysql.sql` — actually exist. If anything is missing it enqueues an error listing what's missing and stops before the success message. Fail-open: any manifest/DB/IO glitch is treated as "nothing missing" so a transient error never turns a good install into a false failure. Unconditional housekeeping (e.g. download-key restore) still runs regardless.
- Pre-update/uninstall backup no longer **white-screens** the update on large sites. The synchronous backup that runs inside the extension update/uninstall request now raises `max_execution_time`/`memory_limit` (and `ignore_user_abort`) like the web-cron path, so it can't exhaust the request's default limits mid-backup. (Core Joomla updates additionally get a full-screen backup screen — see below.)
- Archive names for **CLI/console-triggered backups** no longer come out as `joomla.invalid_…`. The `[HOST]` placeholder took `$_SERVER['HTTP_HOST']` verbatim, but Joomla's console fills that with the reserved sentinel host `joomla.invalid`; the resolver now treats that (like empty/`localhost`) as unusable and falls back to the configured `live_site` host, then the system hostname. (Set the site's *live_site* to get the exact domain in CLI-built names.)
- Standalone restore script generation no longer aborts backups with `str_replace() expects at least 3 arguments, 2 given`. `MokoRestore::generateStandaloneScript()` had a `str_replace()` call (the "Backup Archive" pre-check rewrite) that was missing its `$php` subject argument, so **every** standalone-mode backup fatally errored while "Generating standalone restore.php…" — the archive still finalized and uploaded, but no `restore.php` was ever produced (the true root cause behind #226). (#226)
diff --git a/SECURITY.md b/SECURITY.md
index e892093c..c803c891 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: 02.58.14
+VERSION: 02.58.15
BRIEF: Security vulnerability reporting and handling policy
-->
diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
index 1fca887f..60b7b280 100644
--- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
@@ -17,7 +17,7 @@
display label there.
-->
MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.15.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.15.sql
new file mode 100644
index 00000000..e0816695
--- /dev/null
+++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.58.15.sql
@@ -0,0 +1 @@
+/* 02.58.15 — no schema changes */
diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
index ccd2d3f2..f9e6dc80 100644
--- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
+++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
@@ -8,7 +8,7 @@
-->
Module - MokoSuiteBackup - cPanel
- 02.58.14
+ 02.58.15
2026-06-23
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
index 5f472d70..1ba0bcfb 100644
--- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Action Log - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
index c25a3baa..c5e703bc 100644
--- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Console - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
index 8a8283be..0e03a44a 100644
--- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Content - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
index 7547ab8e..87ffe551 100644
--- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
@@ -1,7 +1,7 @@
Quick Icon - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
index 1a3e1f39..2b76666b 100644
--- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
System - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
index 180126c8..004adea6 100644
--- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Task - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
index dc4681b6..446e8ce2 100644
--- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Web Services - MokoSuiteBackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml
index 98f0fdbe..f72df232 100644
--- a/source/pkg_mokosuitebackup.xml
+++ b/source/pkg_mokosuitebackup.xml
@@ -8,7 +8,7 @@
Package - MokoSuiteBackup
mokosuitebackup
- 02.58.14
+ 02.58.15
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/script.php b/source/script.php
index 65ade9fe..b2f64169 100644
--- a/source/script.php
+++ b/source/script.php
@@ -188,6 +188,35 @@ class Pkg_MokoSuiteBackupInstallerScript
PREPARE/EXECUTE migration that MySQL 8 rejected in Joomla's installer). */
$this->dropLegacyRemoteColumns();
+ /* Honesty gate: Joomla logs a failed child extension but still runs the
+ package postflight, so verify every bundled child actually registered
+ before claiming success. If any are missing, say so and do NOT print the
+ success / next-steps messages. (All housekeeping above runs regardless.)
+ Fail-open — a manifest/DB glitch returns no missing children. */
+ $problems = $this->findMissingPackageChildren($parent);
+
+ foreach ($this->findMissingComponentTables() as $table) {
+ $problems[] = 'table ' . $table;
+ }
+
+ if (!empty($problems)) {
+ $safe = array_map(
+ static fn($m) => htmlspecialchars($m, ENT_QUOTES, 'UTF-8'),
+ $problems
+ );
+ $detail = count($safe) > 3
+ ? count($safe) . ' bundled extensions/tables are missing'
+ : 'missing: ' . implode(', ', $safe);
+
+ Factory::getApplication()->enqueueMessage(
+ 'MokoSuiteBackup did not install correctly — ' . $detail
+ . '. Review the installation log and re-install.',
+ 'error'
+ );
+
+ return;
+ }
+
/* Install completion notice (install and update) */
$this->installSuccessful();
@@ -208,6 +237,133 @@ class Pkg_MokoSuiteBackupInstallerScript
}
}
+ /**
+ * Verify every bundled child extension declared in the package manifest
+ * actually registered in #__extensions. Joomla logs a failed child but does
+ * not fail the package, so this answers "did the package really install?"
+ * honestly. Returns human-readable labels for any missing children (empty =
+ * all present). Matching mirrors extension uniqueness: element + type, and
+ * for plugins also folder (group), since plugins are only unique that way.
+ *
+ * Fail-open: any manifest/DB error returns [] so a transient glitch can never
+ * turn a good install into a false failure.
+ *
+ * @param InstallerAdapter $parent Package installer adapter
+ *
+ * @return string[] Labels of missing children (empty when everything installed)
+ */
+ private function findMissingPackageChildren(InstallerAdapter $parent): array
+ {
+ try {
+ $manifest = $parent->getParent()->getManifest();
+
+ if (!$manifest instanceof \SimpleXMLElement
+ || !isset($manifest->files) || !isset($manifest->files->file)) {
+ return [];
+ }
+
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+ $missing = [];
+
+ foreach ($manifest->files->file as $file) {
+ $attrs = $file->attributes();
+ $element = isset($attrs['id']) ? (string) $attrs['id'] : '';
+ $extType = isset($attrs['type']) ? (string) $attrs['type'] : '';
+
+ if ($element === '' || $extType === '') {
+ continue;
+ }
+
+ $query = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote($element))
+ ->where($db->quoteName('type') . ' = ' . $db->quote($extType));
+
+ $group = '';
+
+ /* Plugins are only unique by element + folder (group). */
+ if ($extType === 'plugin' && isset($attrs['group'])) {
+ $group = (string) $attrs['group'];
+
+ if ($group !== '') {
+ $query->where($db->quoteName('folder') . ' = ' . $db->quote($group));
+ }
+ }
+
+ $db->setQuery($query);
+
+ if ((int) $db->loadResult() === 0) {
+ $label = $extType . ' "' . $element . '"';
+
+ if ($group !== '') {
+ $label .= ' (' . $group . ')';
+ }
+
+ $missing[] = $label;
+ }
+ }
+
+ return $missing;
+ } catch (\Throwable $e) {
+ return [];
+ }
+ }
+
+ /**
+ * Verify the component's schema actually installed. The component has no
+ * installer script of its own, so this package postflight is the only place
+ * to catch a component whose extension row exists but whose CREATE TABLE
+ * statements failed (e.g. an installer-rejected migration). Reads the
+ * *installed* install SQL, extracts every declared table, and confirms each
+ * exists. Table names are derived dynamically — nothing is hard-coded.
+ *
+ * Fail-open: any IO/DB/parse error returns [] so a glitch can't fail a good
+ * install.
+ *
+ * @return string[] Missing table names (empty when all present)
+ */
+ private function findMissingComponentTables(): array
+ {
+ try {
+ $sqlFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/sql/install.mysql.sql';
+
+ if (!is_file($sqlFile)) {
+ return [];
+ }
+
+ $sql = file_get_contents($sqlFile);
+
+ if ($sql === false || $sql === '') {
+ return [];
+ }
+
+ if (!preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(#__[A-Za-z0-9_]+)/i', $sql, $m)) {
+ return [];
+ }
+
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+ $prefix = $db->getPrefix();
+
+ /* Existing tables, compared case-insensitively. */
+ $existing = array_flip(array_map('strtolower', $db->getTableList()));
+
+ $missing = [];
+
+ foreach (array_unique($m[1]) as $declared) {
+ $real = str_replace('#__', $prefix, $declared);
+
+ if (!isset($existing[strtolower($real)])) {
+ $missing[] = $real;
+ }
+ }
+
+ return $missing;
+ } catch (\Throwable $e) {
+ return [];
+ }
+ }
+
/**
* Remove the orphaned component registration created when the component
* briefly used the "Type - Name" display convention. Joomla derives a