diff --git a/public/product-check/index.php b/public/product-check/index.php
new file mode 100644
index 0000000..a0493cd
--- /dev/null
+++ b/public/product-check/index.php
@@ -0,0 +1,328 @@
+ PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
+);
+
+// ---- data -------------------------------------------------------------------
+
+$products = $pdo->query(
+ "SELECT uid, title, slug, teaser, subtitle, description, description2, highlights,
+ capabilities, applications, resources, resources_layout,
+ hidden, legacy, supportproduct, subproduct, hideonwebsite, hideonproducts, showdatapath
+ FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY LOWER(title), uid"
+)->fetchAll();
+
+// FAL references per product and fieldname, with file identifiers
+$fileRefs = $pdo->query(
+ "SELECT r.uid_foreign, r.fieldname, f.identifier, f.name
+ FROM sys_file_reference r JOIN sys_file f ON f.uid = r.uid_local
+ WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_domain_model_product'
+ AND r.fieldname IN ('heroimage','productimage','ogimage','videofile')
+ ORDER BY r.uid_foreign, r.fieldname, r.sorting_foreign"
+)->fetchAll();
+$files = [];
+foreach ($fileRefs as $r) {
+ $files[(int)$r['uid_foreign']][$r['fieldname']][] = ['url' => '/fileadmin' . $r['identifier'], 'name' => (string)$r['name']];
+}
+
+// inline resource cards
+$resourceRows = $pdo->query(
+ 'SELECT * FROM tx_vitec_product_resource WHERE deleted = 0 AND hidden = 0 ORDER BY product, sorting'
+)->fetchAll();
+$resources = [];
+foreach ($resourceRows as $r) {
+ $resources[(int)$r['product']][] = $r;
+}
+
+// MM: datasheets (download records) and solutions, in MM order
+$datasheets = [];
+foreach ($pdo->query(
+ 'SELECT mm.uid_local AS product, d.title FROM tx_vitec_product_datasheet_mm mm
+ JOIN tx_vitec_domain_model_download d ON d.uid = mm.uid_foreign AND d.deleted = 0
+ ORDER BY mm.uid_local, mm.sorting'
+)->fetchAll() as $r) {
+ $datasheets[(int)$r['product']][] = (string)$r['title'];
+}
+$solutions = [];
+foreach ($pdo->query(
+ 'SELECT mm.uid_local AS product, s.title FROM tx_vitec_product_solution_mm mm
+ JOIN tx_vitec_domain_model_solution s ON s.uid = mm.uid_foreign AND s.deleted = 0
+ ORDER BY mm.uid_local, mm.sorting'
+)->fetchAll() as $r) {
+ $solutions[(int)$r['product']][] = (string)$r['title'];
+}
+
+// ---- helpers ------------------------------------------------------------------
+
+function h(string $v): string
+{
+ return htmlspecialchars($v, ENT_QUOTES | ENT_HTML5);
+}
+
+/** check/cross cell; a count > 1 is shown next to the check */
+function cell(bool $ok, int $count = 0): string
+{
+ if (!$ok) {
+ return '
✗ | ';
+ }
+ return '✓' . ($count > 1 ? '' . $count . '' : '') . ' | ';
+}
+
+function filled(?string $v): bool
+{
+ return trim(strip_tags((string)$v)) !== '';
+}
+
+/** labeled block in the detail pane; HTML of RTE fields is rendered */
+function textBlock(string $label, ?string $value): string
+{
+ if (!filled($value)) {
+ return '';
+ }
+ return '' . h($label) . '
' . $value . '
';
+}
+
+/** @param array $images */
+function imageBlock(string $label, array $images): string
+{
+ if ($images === []) {
+ return '';
+ }
+ $out = '' . h($label) . ' (' . count($images) . ')
';
+ foreach ($images as $img) {
+ $out .= '
'
+ . '![' . h($img['name']) . '](' . h($img['url']) . ')
';
+ }
+ return $out . '
';
+}
+
+/** @param string[] $items */
+function listBlock(string $label, array $items): string
+{
+ if ($items === []) {
+ return '';
+ }
+ return '' . h($label) . ' (' . count($items) . ')
- '
+ . implode('
- ', array_map('h', $items)) . '
';
+}
+
+// ---- columns ------------------------------------------------------------------
+
+$columns = ['Hero', 'Photos', 'OG', 'Video', 'Teaser', 'Subtitle', 'Description', 'Descr. 2', 'Highlights', 'Capabilities', 'Applications', 'Resources', 'Datasheets', 'Solutions'];
+
+$filledPerColumn = array_fill_keys($columns, 0);
+$rowsHtml = '';
+foreach ($products as $p) {
+ $uid = (int)$p['uid'];
+ $f = $files[$uid] ?? [];
+ $res = $resources[$uid] ?? [];
+ $ds = $datasheets[$uid] ?? [];
+ $sol = $solutions[$uid] ?? [];
+
+ $checks = [
+ 'Hero' => [count($f['heroimage'] ?? []), true],
+ 'Photos' => [count($f['productimage'] ?? []), true],
+ 'OG' => [count($f['ogimage'] ?? []), true],
+ 'Video' => [count($f['videofile'] ?? []), true],
+ 'Teaser' => [filled($p['teaser']) ? 1 : 0, false],
+ 'Subtitle' => [filled($p['subtitle']) ? 1 : 0, false],
+ 'Description' => [filled($p['description']) ? 1 : 0, false],
+ 'Descr. 2' => [filled($p['description2']) ? 1 : 0, false],
+ 'Highlights' => [filled($p['highlights']) ? 1 : 0, false],
+ 'Capabilities' => [filled($p['capabilities']) ? 1 : 0, false],
+ 'Applications' => [filled($p['applications']) ? 1 : 0, false],
+ 'Resources' => [count($res), true],
+ 'Datasheets' => [count($ds), true],
+ 'Solutions' => [count($sol), true],
+ ];
+
+ $cells = '';
+ foreach ($columns as $column) {
+ [$count, $showCount] = $checks[$column];
+ $filledPerColumn[$column] += $count > 0 ? 1 : 0;
+ $cells .= cell($count > 0, $showCount ? $count : 0);
+ }
+
+ $flags = [];
+ foreach (['hidden' => 'hidden', 'legacy' => 'legacy', 'supportproduct' => 'support', 'subproduct' => 'sub', 'hideonwebsite' => 'hideWeb', 'hideonproducts' => 'hideList', 'showdatapath' => 'datapath'] as $col => $label) {
+ if ((int)$p[$col] === 1) {
+ $flags[] = $label;
+ }
+ }
+
+ // detail pane: every content of the record
+ $resourceLines = [];
+ foreach ($res as $r) {
+ $parts = [];
+ foreach (['title', 'header', 'label', 'kind', 'link'] as $key) {
+ if (isset($r[$key]) && trim((string)$r[$key]) !== '') {
+ $parts[] = (string)$r[$key];
+ }
+ }
+ $resourceLines[] = $parts === [] ? ('#' . $r['uid']) : implode(' · ', $parts);
+ }
+
+ $detail = ''
+ . imageBlock('Hero Image', $f['heroimage'] ?? [])
+ . imageBlock('Product Images', $f['productimage'] ?? [])
+ . imageBlock('OG Image', $f['ogimage'] ?? [])
+ . listBlock('Video Files', array_map(static fn(array $v): string => $v['name'] . ' (' . $v['url'] . ')', $f['videofile'] ?? []))
+ . textBlock('Teaser', (string)$p['teaser'])
+ . textBlock('Subtitle', (string)$p['subtitle'])
+ . textBlock('Description', (string)$p['description'])
+ . textBlock('Description 2', (string)$p['description2'])
+ . textBlock('Highlights', (string)$p['highlights'])
+ . textBlock('Capabilities', (string)$p['capabilities'])
+ . textBlock('Applications', (string)$p['applications'])
+ . listBlock('Resources', $resourceLines)
+ . listBlock('Datasheets', $ds)
+ . listBlock('Solutions', $sol);
+
+ $rowsHtml .= ''
+ . '| ▸' . h((string)$p['title'])
+ . ($flags !== [] ? ' ' . h(implode(' ', $flags)) . '' : '')
+ . ' | ' . $cells . '
' . "\n"
+ . '' . $detail . ' |
' . "\n";
+}
+
+$headCells = 'Product (' . count($products) . ') | ';
+foreach ($columns as $column) {
+ $headCells .= ''
+ . h($column) . '' . $filledPerColumn[$column] . ' | ';
+}
+
+// ---- page ---------------------------------------------------------------------
+?>
+
+
+
+
+
+
+VITEC Product Content Check
+
+
+
+
+ VITEC Product Content Check
+ = count($products) ?> products · as of = h(date('Y-m-d H:i:s')) ?> · click a row for details
+
+
+
+
+= $headCells ?>
+
+= $rowsHtml ?>
+
+
+
+
+
+
+