From b0690c4cdcd2e4a5c87aee8772f8f2c913f3fdf2 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 27 Oct 2026 15:10:24 +0100 Subject: [PATCH] Cards visible in the FE; /products cards wrapped into 33/33/33 grids - the freshly attached card images did not show: text_only is the explicit no-image variant, so vitec:attach-card-images now flips a card to "vertical" whenever it carries (or receives) an image; and the new placeholder PNGs sat in the sshfs 770 trap - fix-asset-perms covers them, with public/product-audit added to its targets - new generic vitec:wrap-cards-grid --page=: wraps a page's cards into vitec_cols_33_33_33 containers (b13 columns 221/222/223), three per container, section-aware - every top-level intro paragraph starts a section, other element types close it, so the category headings on /products keep their grids beneath them. Existing grid containers are pooled and reused, surplus ones hidden - the first apply aborted mid-move and left a half state (15 empty shells, three cards moved; diagnosis migrations/check_grid_447.php, kept) - v2 repairs such states by design: global container pool, hard asserts after the create pass, try/catch around the move pass, and a final "no card left top-level" verification - applied on /products and verified live: 38 cards in 15 grids (sections 5/7/3/6/2/4/2/6/3 -> 2/3/1/2/1/2/1/2/1 containers), order intro -> grids -> banner -> faq intact, children delivered in the known items/contentElements shape Co-Authored-By: Claude Fable 5 --- fix-asset-perms.sh | 3 +- migrations/check_grid_447.php | 41 +++ .../Command/AttachCardImagesCommand.php | 19 +- .../Command/WrapCardsInGridCommand.php | 243 ++++++++++++++++++ 4 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 migrations/check_grid_447.php create mode 100644 packages/vitec/Classes/Command/WrapCardsInGridCommand.php diff --git a/fix-asset-perms.sh b/fix-asset-perms.sh index ad2a0f4..7d59fa9 100755 --- a/fix-asset-perms.sh +++ b/fix-asset-perms.sh @@ -20,7 +20,8 @@ echo "[perms] Project root: $(pwd)" # importer output lands there (success_stories_import, blog-import, downloads, # user_upload/products ...) and hits the same 403 otherwise. Found 2026-09-11, # when the imported success-story images stayed invisible in the megamenu. -TARGETS="public/_assets public/fileadmin packages/vitec/Resources/Public" +# public/product-audit: generated report page (build_product_audit.php), same 403 trap +TARGETS="public/_assets public/fileadmin public/product-audit packages/vitec/Resources/Public" echo "[perms] Setting files 644 / directories 755 under $TARGETS ..." find $TARGETS -type f -exec chmod 644 {} \; 2>/dev/null diff --git a/migrations/check_grid_447.php b/migrations/check_grid_447.php new file mode 100644 index 0000000..19d24d2 --- /dev/null +++ b/migrations/check_grid_447.php @@ -0,0 +1,41 @@ + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] +); + +echo "=== all vitec_card / vitec_cols_33_33_33 / vitec_introparagraph on pid 447 ===\n"; +$rows = $pdo->query( + "SELECT uid, CType, LEFT(header,32) h, colPos, tx_container_parent par, sorting, hidden, deleted + FROM tt_content WHERE pid = 447 AND CType IN ('vitec_card','vitec_cols_33_33_33','vitec_introparagraph') + ORDER BY deleted, colPos, sorting, uid" +)->fetchAll(); +foreach ($rows as $r) { + printf(" #%-6d %-22s col=%-4d par=%-6d sort=%-6d hid=%d del=%d %s\n", + $r['uid'], $r['CType'], $r['colPos'], $r['par'], $r['sorting'], $r['hidden'], $r['deleted'], $r['h']); +} +echo "\ncounts: " . count($rows) . " rows\n"; diff --git a/packages/vitec/Classes/Command/AttachCardImagesCommand.php b/packages/vitec/Classes/Command/AttachCardImagesCommand.php index 0b6a2c8..9902e0c 100644 --- a/packages/vitec/Classes/Command/AttachCardImagesCommand.php +++ b/packages/vitec/Classes/Command/AttachCardImagesCommand.php @@ -77,10 +77,10 @@ final class AttachCardImagesCommand extends Command $datamap = []; $newIndex = 0; - $stats = ['photo' => 0, 'placeholder' => 0, 'grey' => 0, 'kept' => 0, 'missing' => []]; + $stats = ['photo' => 0, 'placeholder' => 0, 'grey' => 0, 'kept' => 0, 'variant' => 0, 'missing' => []]; foreach ($pageUids as $pageUid => $slug) { $cards = $content->fetchAllAssociative( - "SELECT uid, header, vitec_primary_cta FROM tt_content + "SELECT uid, header, vitec_primary_cta, vitec_layout_variant FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = ? AND CType = 'vitec_card' ORDER BY colPos, sorting", [$pageUid] @@ -104,6 +104,13 @@ final class AttachCardImagesCommand extends Command ); if ($existing > 0) { $stats['kept']++; + // a card that carries an image must not stay on the + // explicit no-image variant (found 2026-10-27: the FE + // correctly hides images on text_only) + if ((string)$card['vitec_layout_variant'] === 'text_only') { + $datamap['tt_content'][(string)$cardUid]['vitec_layout_variant'] = 'vertical'; + $stats['variant']++; + } continue; } $identifier = null; @@ -138,6 +145,10 @@ final class AttachCardImagesCommand extends Command if (!$apply) { continue; } + if ((string)$card['vitec_layout_variant'] === 'text_only') { + $datamap['tt_content'][(string)$cardUid]['vitec_layout_variant'] = 'vertical'; + $stats['variant']++; + } $file = $storage->getFile($identifier); $datamap['sys_file_reference']['NEWCI' . (++$newIndex)] = [ 'pid' => $pageUid, @@ -150,8 +161,8 @@ final class AttachCardImagesCommand extends Command } } - $output->writeln(sprintf("\n%d product photo(s), %d numbered placeholder(s), %d generic grey, %d card(s) already had an image%s", - $stats['photo'], $stats['placeholder'], $stats['grey'], $stats['kept'], + $output->writeln(sprintf("\n%d product photo(s), %d numbered placeholder(s), %d generic grey, %d card(s) already had an image, %d variant flip(s) text_only->vertical%s", + $stats['photo'], $stats['placeholder'], $stats['grey'], $stats['kept'], $stats['variant'], $stats['missing'] === [] ? '' : ' · MISSING numbered files: ' . implode(', ', $stats['missing']))); if (!$apply) { diff --git a/packages/vitec/Classes/Command/WrapCardsInGridCommand.php b/packages/vitec/Classes/Command/WrapCardsInGridCommand.php new file mode 100644 index 0000000..6753fd0 --- /dev/null +++ b/packages/vitec/Classes/Command/WrapCardsInGridCommand.php @@ -0,0 +1,243 @@ +addOption('page', null, InputOption::VALUE_REQUIRED, 'Slug of the page, e.g. /products'); + $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + $apply = (bool)$input->getOption('apply'); + $slug = '/' . trim((string)$input->getOption('page'), '/ '); + $pool = GeneralUtility::makeInstance(ConnectionPool::class); + $pages = $pool->getConnectionForTable('pages'); + $content = $pool->getConnectionForTable('tt_content'); + + $page = $pages->fetchAssociative('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)', [$slug]); + if (!$page) { + $output->writeln('Page ' . $slug . ' not found.'); + return Command::FAILURE; + } + $pageUid = (int)$page['uid']; + $elements = $content->fetchAllAssociative( + 'SELECT uid, CType, header, hidden FROM tt_content + WHERE deleted = 0 AND pid = ? AND colPos = ? AND tx_container_parent = 0 AND sys_language_uid IN (0,-1) + ORDER BY sorting', + [$pageUid, self::COLPOS_MAIN] + ); + $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · %d top-level element(s) in main', $slug, $pageUid, count($elements))); + + // --- parse into an ordered plan ------------------------------------------------- + // order entries: ['el', uid] | ['section', ['intro'=>?uid,'cards'=>[uids]]] + // Containers go into ONE global pool: a half-applied earlier run may + // have left empty grids anywhere on the page - every card gets an + // explicit column assignment anyway, so the shells are interchangeable. + $order = []; + $section = null; + $pool = []; + $flush = function () use (&$order, &$section): void { + if ($section !== null) { + $order[] = ['section', $section]; + $section = null; + } + }; + foreach ($elements as $element) { + $uid = (int)$element['uid']; + $ctype = (string)$element['CType']; + if ((int)$element['hidden'] === 1) { + continue; // hidden legacy stays untouched and unsequenced + } + if ($ctype === 'vitec_introparagraph') { + $flush(); + $section = ['intro' => $uid, 'cards' => []]; + continue; + } + if ($ctype === 'vitec_card') { + if ($section === null) { + $section = ['intro' => null, 'cards' => []]; + } + $section['cards'][] = $uid; + continue; + } + if ($ctype === self::CONTAINER) { + $pool[] = $uid; + $children = $content->fetchAllAssociative( + "SELECT uid FROM tt_content WHERE deleted = 0 AND hidden = 0 AND tx_container_parent = ? AND CType = 'vitec_card' ORDER BY colPos, sorting", + [$uid] + ); + if ($children !== []) { + if ($section === null) { + $section = ['intro' => null, 'cards' => []]; + } + foreach ($children as $child) { + $section['cards'][] = (int)$child['uid']; + } + } + continue; + } + $flush(); + $order[] = ['el', $uid]; + } + $flush(); + + // --- plan containers per section -------------------------------------------------- + $datamap = []; + $newIndex = 0; + $newContainers = []; + $sequenceTop = []; + $cardAssignments = []; // containerId(string) => [cardUid,...] max 3 + $hiddenContainers = 0; + foreach ($order as [$type, $value]) { + if ($type === 'el') { + $sequenceTop[] = (string)$value; + continue; + } + $sectionPlan = $value; + if ($sectionPlan['intro'] !== null) { + $sequenceTop[] = (string)$sectionPlan['intro']; + } + $cards = $sectionPlan['cards']; + $needed = (int)ceil(count($cards) / 3); + for ($i = 0; $i < $needed; $i++) { + $containerId = array_shift($pool); + if ($containerId !== null) { + $containerId = (string)$containerId; + } else { + $containerId = 'NEWGRID' . (++$newIndex); + $newContainers[$containerId] = ['pid' => $pageUid, 'CType' => self::CONTAINER, 'colPos' => self::COLPOS_MAIN]; + } + $sequenceTop[] = $containerId; + $cardAssignments[$containerId] = array_slice($cards, $i * 3, 3); + } + $intro = $sectionPlan['intro'] !== null ? '#' . $sectionPlan['intro'] : '(no intro)'; + $output->writeln(sprintf(' section %s: %d card(s) -> %d container(s)', $intro, count($cards), $needed)); + } + foreach ($pool as $surplus) { + $datamap['tt_content'][(string)(int)$surplus]['hidden'] = 1; + $hiddenContainers++; + } + if ($hiddenContainers > 0) { + $output->writeln(' ' . $hiddenContainers . ' surplus grid container(s) hidden'); + } + $output->writeln(sprintf(' plan: %d new container(s), %d card(s) distributed', count($newContainers), array_sum(array_map('count', $cardAssignments)))); + + if (!$apply) { + $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); + return Command::SUCCESS; + } + + // pass A: create missing containers + $idMap = []; + if ($newContainers !== []) { + $dhA = GeneralUtility::makeInstance(DataHandler::class); + $dhA->start(['tt_content' => $newContainers], []); + $dhA->process_datamap(); + if ($dhA->errorLog !== []) { + $output->writeln('DataHandler containers: ' . implode(' | ', $dhA->errorLog) . ''); + return Command::FAILURE; + } + foreach ($newContainers as $newId => $fields) { + $idMap[$newId] = (int)($dhA->substNEWwithIDs[$newId] ?? 0); + if ($idMap[$newId] <= 0) { + $output->writeln('Container ' . $newId . ' was not created - aborting before any move.'); + return Command::FAILURE; + } + } + } + $resolve = static fn($id): int => str_starts_with((string)$id, 'NEWGRID') ? (int)($idMap[(string)$id] ?? 0) : (int)$id; + + // pass B: move the cards into the container columns + foreach ($cardAssignments as $containerId => $cards) { + $containerUid = $resolve($containerId); + if ($containerUid <= 0) { + continue; + } + foreach ($cards as $i => $cardUid) { + $datamap['tt_content'][(string)$cardUid] = [ + 'colPos' => self::COLS[$i % 3], + 'tx_container_parent' => $containerUid, + ]; + } + } + if ($datamap !== []) { + try { + $dhB = GeneralUtility::makeInstance(DataHandler::class); + $dhB->start($datamap, []); + $dhB->process_datamap(); + } catch (\Throwable $e) { + $output->writeln('Move pass threw: ' . get_class($e) . ' - ' . $e->getMessage() . ''); + return Command::FAILURE; + } + if ($dhB->errorLog !== []) { + $output->writeln('DataHandler cards: ' . implode(' | ', $dhB->errorLog) . ''); + } + } + // top-level order + $sorting = 0; + foreach ($sequenceTop as $id) { + $uid = $resolve($id); + if ($uid > 0) { + $sorting += 256; + $content->update('tt_content', ['sorting' => $sorting], ['uid' => $uid]); + } + } + // card sorting inside the columns (one card per column and container) + foreach ($cardAssignments as $containerId => $cards) { + foreach ($cards as $cardUid) { + $content->update('tt_content', ['sorting' => 256], ['uid' => (int)$cardUid]); + } + } + $left = (int)$content->fetchOne( + "SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = ? AND CType = 'vitec_card' AND tx_container_parent = 0 AND colPos = ?", + [$pageUid, self::COLPOS_MAIN] + ); + if ($left > 0) { + $output->writeln('' . $left . ' card(s) are STILL top-level - inspect with migrations/check_grid_447.php'); + return Command::FAILURE; + } + $output->writeln('Done - every card sits in a grid container. Flush the frontend cache.'); + return Command::SUCCESS; + } +}