Success stories: downloads and product sections become real plugins

- comparison report migrations/check_product_downloads.php (kept,
  read-only): download records vs. the delivered product workbooks -
  34 of 74 workbook products have a datasheet, the entire Datapath
  line has none in the pool (import came from vitec.com; queued for
  Joe), ~35 orphan datasheets belong to products without records;
  a vitec:link-product-downloads re-run added seven clean links
  (Artio x2, Diamond-H, the three TOUGH records, X-View)
- RTE resolution fixed at the root: ContentElementResolver and
  ContainerChildrenProcessor passed bodytext RAW into the payload, so
  t3:// links leaked unresolved; both now run RTE columns (bodytext,
  vitec_quote) through RteResolver::html()
- the imported "Download Success Story" RTE buttons carried the wrong
  link identifier (tx_vitec_download -> download, one-time migration
  fix_download_record_links.php) AND old-site uids - missing here or,
  worse, accidentally pointing at foreign PDFs. New
  vitec:convert-story-download-buttons replaces them with real
  vitec_downloadcard plugins: the download is resolved by STORY title
  (manual map for ISR / "Okada Manilla", exact-title fallback for
  suffixless PDFs like "Lord's Cricket Ground"); 29 converted, 4
  stories without a delivered PDF have their button hidden. New
  FlexForm layout "Success Story Download" (story_download) applied
  everywhere; the FE renders it as the previous centered button
- the ADIB "Featured Products" text section (the only one site-wide,
  verified by scan_story_headers.php) is now ONE vitec_productlist
  with hand-picked products via old-name aliases and the new FlexForm
  layout "Success Story Products" (story_products); the four text
  elements are deleted. vitec:convert-story-product-sections stays
  re-runnable for future imports
- the productlist envelope additionally carries "columns" (= number
  of products, capped at 4) so the story grid renders as many columns
  as it has products - additive key, available to every product list;
  and hand-picked lists now ship successStories as an empty array (the
  empty category selection used to aggregate stories over the WHOLE
  tree, leaking story cards into the section)
- the story downloadcards carry header "Downloads" (set on convert
  and backfilled): the front end builds the story anchor nav from
  element headers, so the section now appears there
- diagnosis scripts kept: check_download_record, check_story_elements,
  scan_story_headers; verified live on the ADIB payload (structured
  download object, productlist [5,18,6], texts 10 -> 5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-10-28 14:10:59 +01:00
1 parent 80f6da7949
commit d87e64f73b
12 files changed
+763 -2

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
// Diagnose: state of one download record (uid as argv). READ-ONLY.
error_reporting(E_ALL); ini_set('display_errors', '1');
$uid = (int)($argv[1] ?? 0);
$root = null; $dir = __DIR__;
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
if ($root === null) { exit("no root\n"); }
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$r = $pdo->query("SELECT uid, pid, title, slug, hidden, deleted, starttime, endtime, private_download, file, fileprefix, useolddl FROM tx_vitec_domain_model_download WHERE uid = $uid")->fetch();
print_r($r ?: "not found\n");
if ($r) {
foreach ($pdo->query("SELECT r.uid, r.uid_local, f.identifier FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local WHERE r.deleted = 0 AND r.tablenames = 'tx_vitec_domain_model_download' AND r.uid_foreign = $uid") as $ref) { print_r($ref); }
}
+127
View File
@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
/**
* Comparison report: download records vs. the delivered product workbooks
* (request Olli 2026-10-28). Per product: linked downloads
* (tx_vitec_product_download_mm, uid_local = product), datasheet coverage
* (download title suffix "datasheet" or a Datasheet category), the manual
* Resources-tab selection (tx_vitec_product_datasheet_mm) - matched against
* the workbook coverage from migrations/products_xlsx/manifest.json.
*
* READ-ONLY. Usage: php migrations/check_product_downloads.php
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
$root = null;
$dir = __DIR__;
for ($i = 0; $i < 5; $i++) {
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
$dir = dirname($dir);
}
if ($root === null) { exit("no project root\n"); }
$settings = require $root . '/config/system/settings.php';
$db = $settings['DB']['Connections']['Default'];
$pdo = new PDO(
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
$db['user'] ?? '', $db['password'] ?? '',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
// --- workbook coverage from the manifest ------------------------------------
$manifest = json_decode((string)file_get_contents($root . '/migrations/products_xlsx/manifest.json'), true) ?: [];
$workbookSlugs = [];
foreach (($manifest['workbooks'] ?? []) as $file => $entry) {
if (!empty($entry['product'])) {
$workbookSlugs[(string)$entry['product']] = basename((string)$file);
}
}
// --- data ---------------------------------------------------------------------
$products = $pdo->query('SELECT uid, title, slug, hidden, legacy, supportproduct, subproduct FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title')->fetchAll();
$downloads = $pdo->query('SELECT uid, title, hidden FROM tx_vitec_domain_model_download WHERE deleted = 0')->fetchAll();
$downloadTitle = array_column($downloads, 'title', 'uid');
$linksByProduct = [];
$linkedDownloadUids = [];
foreach ($pdo->query('SELECT uid_local, uid_foreign FROM tx_vitec_product_download_mm') as $r) {
$linksByProduct[(int)$r['uid_local']][] = (int)$r['uid_foreign'];
$linkedDownloadUids[(int)$r['uid_foreign']] = true;
}
$sheetsByProduct = [];
foreach ($pdo->query('SELECT uid_local, COUNT(*) c FROM tx_vitec_product_datasheet_mm GROUP BY uid_local') as $r) {
$sheetsByProduct[(int)$r['uid_local']] = (int)$r['c'];
}
$isDatasheet = static function (string $title): bool {
return (bool)preg_match('/data\s*sheet\s*$/i', trim($title));
};
$docType = static function (string $title): string {
foreach (['datasheet', 'data sheet', 'brochure', 'user guide', 'quick start guide', 'application note',
'white paper', 'case study', 'success story', 'manual', 'firmware', 'software', 'flyer', 'guide'] as $suffix) {
if (preg_match('/' . preg_quote($suffix, '/') . '\s*$/i', trim($title))) {
return $suffix;
}
}
return '(no type suffix)';
};
// --- per-product evaluation ------------------------------------------------------
$noDownloads = [];
$noDatasheet = [];
$okCount = 0;
$withoutWorkbook = [];
foreach ($products as $p) {
$uid = (int)$p['uid'];
$slug = (string)$p['slug'];
$linked = $linksByProduct[$uid] ?? [];
$hasSheet = false;
foreach ($linked as $dUid) {
if ($isDatasheet((string)($downloadTitle[$dUid] ?? ''))) { $hasSheet = true; break; }
}
$flag = ((int)$p['legacy'] ? ' [legacy]' : '') . ((int)$p['hidden'] ? ' [hidden]' : '')
. ((int)$p['supportproduct'] ? ' [support]' : '') . ((int)$p['subproduct'] ? ' [sub]' : '');
$line = sprintf('#%-4d %-45s dl=%-2d sheetsTab=%d%s', $uid, $p['title'], count($linked), $sheetsByProduct[$uid] ?? 0, $flag);
if (!isset($workbookSlugs[$slug])) {
$withoutWorkbook[] = $line;
}
if (isset($workbookSlugs[$slug])) {
if ($linked === []) { $noDownloads[] = $line; }
elseif (!$hasSheet) { $noDatasheet[] = $line; }
else { $okCount++; }
}
}
// --- orphan downloads ---------------------------------------------------------------
$orphans = [];
$orphanTypes = [];
foreach ($downloads as $d) {
if (!isset($linkedDownloadUids[(int)$d['uid']])) {
$type = $docType((string)$d['title']);
$orphanTypes[$type] = ($orphanTypes[$type] ?? 0) + 1;
$orphans[] = $d['title'];
}
}
printf("Produkte: %d (davon %d mit Workbook) · Downloads: %d (davon %d verlinkt, %d verwaist)\n\n",
count($products), count($workbookSlugs), count($downloads), count($linkedDownloadUids), count($orphans));
echo "=== A) Workbook-Produkte OHNE verknuepfte Downloads (" . count($noDownloads) . ") ===\n";
foreach ($noDownloads as $l) { echo " $l\n"; }
echo "\n=== B) Workbook-Produkte mit Downloads, aber OHNE Datasheet (" . count($noDatasheet) . ") ===\n";
foreach ($noDatasheet as $l) { echo " $l\n"; }
echo "\n=== C) Workbook-Produkte mit Datasheet-Abdeckung: $okCount ===\n";
echo "\n=== D) Produkte OHNE Workbook (" . count($withoutWorkbook) . ") - zur Einordnung ===\n";
foreach ($withoutWorkbook as $l) { echo " $l\n"; }
arsort($orphanTypes);
echo "\n=== E) Verwaiste Downloads nach Typ ===\n";
foreach ($orphanTypes as $type => $n) { printf(" %-22s %d\n", $type, $n); }
echo "\n Beispiele (max 30):\n";
foreach (array_slice($orphans, 0, 30) as $t) { echo " - $t\n"; }
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
// Diagnose: ordered content elements of one usecase (argv: usecase uid,
// default 12 = ADIB) incl. columns children. READ-ONLY.
error_reporting(E_ALL); ini_set('display_errors', '1');
$ucUid = (int)($argv[1] ?? 12);
$root = null; $dir = __DIR__;
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
if ($root === null) { exit("no root\n"); }
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$story = $pdo->query("SELECT uid, title, products FROM tx_vitec_domain_model_usecase WHERE uid = $ucUid")->fetch();
echo "Story #{$story['uid']} \"{$story['title']}\" products-MM-count-field={$story['products']}\n";
echo "verknuepfte Produkte: ";
foreach ($pdo->query("SELECT p.uid, p.title FROM tx_vitec_usecase_product_mm mm JOIN tx_vitec_domain_model_product p ON p.uid = mm.uid_foreign WHERE mm.uid_local = $ucUid ORDER BY mm.sorting") as $p) {
echo "#{$p['uid']} {$p['title']} | ";
}
echo "\n\n";
function dump(PDO $pdo, string $where, int $depth): void {
foreach ($pdo->query("SELECT uid, CType, LEFT(header,45) h, colPos, sorting, hidden, LEFT(REPLACE(REPLACE(COALESCE(bodytext,''),CHAR(10),' '),CHAR(13),''),70) b FROM tt_content WHERE deleted = 0 AND $where ORDER BY sorting") as $r) {
printf("%s#%-5d %-20s col=%-4d sort=%-6d hid=%d %-45s | %s\n", str_repeat(' ', $depth), $r['uid'], $r['CType'], $r['colPos'], $r['sorting'], $r['hidden'], $r['h'], $r['b']);
dump($pdo, "tx_container_parent = {$r['uid']}", $depth + 1);
}
}
dump($pdo, "tx_vitec_usecase_content = $ucUid AND tx_container_parent = 0", 0);
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* Repairs the record links the success-story import wrote into RTE bodies:
* it used t3://record?identifier=tx_vitec_download&uid=N (the TABLE name),
* but the configured link handler key - and therefore the only identifier
* config.recordLinks resolves - is `download`. Result: raw t3://record
* hrefs leaked into the JSON payload ("Download Success Story" buttons,
* found 2026-10-28).
*
* This migration rewrites identifier=tx_vitec_download -> identifier=download
* in tt_content.bodytext (plain and &amp;-encoded variants). One-time,
* idempotent, dry-run by default.
*
* Usage: php migrations/fix_download_record_links.php (dry run)
* php migrations/fix_download_record_links.php --apply
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
$apply = in_array('--apply', $argv, true);
$root = null;
$dir = __DIR__;
for ($i = 0; $i < 5; $i++) {
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
$dir = dirname($dir);
}
if ($root === null) { exit("no project root\n"); }
$settings = require $root . '/config/system/settings.php';
$db = $settings['DB']['Connections']['Default'];
$pdo = new PDO(
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
$db['user'] ?? '', $db['password'] ?? '',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
$needle = 'identifier=tx_vitec_download';
$rows = $pdo->query(
"SELECT uid, pid, CType, LEFT(header, 40) h FROM tt_content
WHERE deleted = 0 AND bodytext LIKE '%identifier=tx_vitec_download%' ORDER BY pid, uid"
)->fetchAll();
echo ($apply ? "APPLY\n" : "DRY-RUN\n");
echo count($rows) . " element(s) carry the legacy identifier:\n";
foreach ($rows as $r) {
printf(" #%-6d pid=%-5d %-18s %s\n", $r['uid'], $r['pid'], $r['CType'], $r['h']);
}
if (!$apply) {
exit("DRY RUN - add --apply to rewrite identifier=tx_vitec_download -> identifier=download.\n");
}
$stmt = $pdo->prepare(
"UPDATE tt_content SET bodytext = REPLACE(bodytext, 'identifier=tx_vitec_download', 'identifier=download'),
tstamp = UNIX_TIMESTAMP() WHERE uid = ?"
);
foreach ($rows as $r) {
$stmt->execute([(int)$r['uid']]);
}
echo count($rows) . " element(s) rewritten. Flush the cache, then re-check a story payload.\n";
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
// Scan: all text-element headers inside usecase content, grouped. READ-ONLY.
error_reporting(E_ALL); ini_set('display_errors', '1');
$root = null; $dir = __DIR__;
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
if ($root === null) { exit("no root\n"); }
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
echo "=== Header von text-Elementen in Story-Content (gruppiert) ===\n";
foreach ($pdo->query("SELECT TRIM(header) h, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 AND CType = 'text' AND TRIM(COALESCE(header,'')) <> '' GROUP BY TRIM(header) ORDER BY c DESC, h") as $r) {
printf(" %3d x %s\n", $r['c'], $r['h']);
}
echo "\n=== CTypes in Story-Content gesamt ===\n";
foreach ($pdo->query("SELECT CType, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 GROUP BY CType ORDER BY c DESC") as $r) {
printf(" %3d x %s\n", $r['c'], $r['CType']);
}
echo "\n=== Header von Kind-Elementen in Story-vitec_columns ===\n";
foreach ($pdo->query(
"SELECT TRIM(c.header) h, c.CType, COUNT(*) n FROM tt_content c
WHERE c.deleted = 0 AND TRIM(COALESCE(c.header,'')) <> ''
AND c.tx_container_parent IN (
SELECT p.uid FROM tt_content p
WHERE p.deleted = 0 AND p.tx_vitec_usecase_content > 0 AND p.CType = 'vitec_columns')
GROUP BY TRIM(c.header), c.CType ORDER BY n DESC, h"
) as $r) {
printf(" %3d x %-14s %s\n", $r['n'], $r['CType'], $r['h']);
}