Files
VITEC-website/migrations/build_product_audit.php
T
orascheandClaude Fable 5 e2d4367d7a Products run: missing records, asset re-run, audit page, category pages
- new vitec:create-missing-products: MGW Diamond TOUGH, MGW Pico TOUGH,
  MGW Pico+ TOUGH and X-View Integrated Displays as visible records
  (teaser drafts, pid and sys_category assignments copied from the
  sibling product; titles equal the Dropbox folder names so the image
  importer matches immediately). IQS4 stays out - its delivery folder
  is empty, question to Joe
- asset re-run via vitec:import-product-images after re-staging the
  Dropbox delivery (the misfiled Diamond-OG banner corrected into the
  OG inbox folder again): new photos for 95-Series, Avedia chassis,
  SFF PRISM and the new TOUGH/X-View records
- new migrations/build_product_audit.php: generates the internal
  product-asset audit at /product-audit/ (same basic auth, same-origin
  thumbnails) - one row per record with thumbnail, detail link, badges
  for hero/photos/texts/resources/datasheets/solutions, flags, stats
  and client-side filters; re-run any time
- new vitec:import-product-category-pages: builds the nine
  /products/<category> pages (frontend layout 9, hero/main/preFooter)
  from the agency "Product Category Pages" workbooks - intro
  paragraphs, Why-VITEC / product / solution cards, the comparison
  rows as ONE text element with a plain table (no comparison CType
  exists; FE styles it), prefooter banner to /contact, full page
  properties, page sorting per sitemap 3.1-3.9; the empty shells #455
  and #457 are reused. Product card links resolve record-first (exact
  title, stripped parenthesis, family alias, unique slug prefix);
  ProductFamilyAliases gains ten category-level family labels. Solution
  cards tolerate US/UK spelling (center/centre, visualization/
  visualisation). Applied and verified live (nine routes, links,
  tables, meta)
- known gaps reported, queued for Joe: IQS4, Media Players (93/94),
  Chassis Systems, Express Backplanes, Accessories & Cables, PCIe
  Encoders, VMS Plugins & Integrations; the Vision family card awaits
  a flagship decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 13:31:03 +01:00

216 lines
10 KiB
PHP

<?php
declare(strict_types=1);
/**
* Builds the internal product-asset audit page: one row per product record
* with thumbnail and badges for everything we track (hero, photos, texts,
* resources, datasheets, solutions, flags). Output is a static page under
* the dev host's basic auth:
*
* public/product-audit/index.html -> https://dev.vitec.com/product-audit/
*
* Plain PDO against config/system/settings.php (pattern
* migrations/check_bg.php), re-run any time the data changes:
*
* php migrations/build_product_audit.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("Could not locate project root above " . __DIR__ . "\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]
);
// ---- data -----------------------------------------------------------------
$products = $pdo->query(
"SELECT uid, title, slug, teaser, subtitle, description, description2, highlights,
capabilities, applications, resources, resources_layout, datasheets, solutions,
hidden, legacy, supportproduct, subproduct, hideonwebsite, hideonproducts, showdatapath
FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title"
)->fetchAll();
$refs = $pdo->query(
"SELECT r.uid_foreign, r.fieldname, COUNT(*) AS cnt, MIN(r.sorting_foreign) AS minsort
FROM sys_file_reference r
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_domain_model_product'
AND r.fieldname IN ('heroimage','productimage','ogimage','videofile')
GROUP BY r.uid_foreign, r.fieldname"
)->fetchAll();
$refCount = [];
foreach ($refs as $r) {
$refCount[(int)$r['uid_foreign']][$r['fieldname']] = (int)$r['cnt'];
}
// first thumbnail per product: prefer a product photo, fall back to the hero
$thumbRows = $pdo->query(
"SELECT r.uid_foreign, r.fieldname, f.identifier
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')
ORDER BY r.uid_foreign, FIELD(r.fieldname,'productimage','heroimage'), r.sorting_foreign"
)->fetchAll();
$thumb = [];
foreach ($thumbRows as $r) {
$uid = (int)$r['uid_foreign'];
if (!isset($thumb[$uid])) {
$thumb[$uid] = '/fileadmin' . $r['identifier'];
}
}
// ---- helpers ----------------------------------------------------------------
function h(string $v): string
{
return htmlspecialchars($v, ENT_QUOTES | ENT_HTML5);
}
function badge(string $label, bool $ok, string $detail = ''): string
{
$cls = $ok ? 'ok' : 'miss';
$txt = $label . ($detail !== '' ? ' ' . $detail : '');
return '<span class="b ' . $cls . '">' . h($txt) . '</span>';
}
// ---- stats ------------------------------------------------------------------
$total = count($products);
$stat = ['hero' => 0, 'photo' => 0, 'teaser' => 0, 'highlights' => 0, 'description' => 0, 'resources' => 0, 'datasheets' => 0, 'solutions' => 0];
foreach ($products as $p) {
$uid = (int)$p['uid'];
if (($refCount[$uid]['heroimage'] ?? 0) > 0) { $stat['hero']++; }
if (($refCount[$uid]['productimage'] ?? 0) > 0) { $stat['photo']++; }
if (trim((string)$p['teaser']) !== '') { $stat['teaser']++; }
if (trim((string)$p['highlights']) !== '') { $stat['highlights']++; }
if (trim((string)$p['description']) !== '') { $stat['description']++; }
if ((int)$p['resources'] > 0) { $stat['resources']++; }
if ((int)$p['datasheets'] > 0) { $stat['datasheets']++; }
if ((int)$p['solutions'] > 0) { $stat['solutions']++; }
}
// ---- rows -------------------------------------------------------------------
$rowsHtml = '';
foreach ($products as $p) {
$uid = (int)$p['uid'];
$hero = $refCount[$uid]['heroimage'] ?? 0;
$photos = $refCount[$uid]['productimage'] ?? 0;
$og = $refCount[$uid]['ogimage'] ?? 0;
$video = $refCount[$uid]['videofile'] ?? 0;
$hasTeaser = trim((string)$p['teaser']) !== '';
$hasHighlights = trim((string)$p['highlights']) !== '';
$hasDescription = trim((string)$p['description']) !== '';
$classes = ['row'];
if ($hero === 0) { $classes[] = 'f-nohero'; }
if ($photos === 0) { $classes[] = 'f-nophoto'; }
if (!$hasTeaser) { $classes[] = 'f-noteaser'; }
if (!$hasHighlights) { $classes[] = 'f-nohighlights'; }
if ((int)$p['legacy'] === 1) { $classes[] = 'f-legacy'; }
if ((int)$p['hidden'] === 1 || (int)$p['hideonwebsite'] === 1) { $classes[] = 'f-hidden'; }
$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[] = '<span class="b flag">' . $label . '</span>';
}
}
$img = isset($thumb[$uid])
? '<img loading="lazy" src="' . h($thumb[$uid]) . '" alt="">'
: '<div class="noimg">no image</div>';
$slug = trim((string)$p['slug'], '/');
$rowsHtml .= '<div class="' . implode(' ', $classes) . '">'
. '<div class="thumb">' . $img . '</div>'
. '<div class="info">'
. '<div class="t"><a href="/product/' . h($slug) . '" target="_blank">' . h((string)$p['title']) . '</a>'
. ' <span class="uid">#' . $uid . '</span></div>'
. '<div class="badges">'
. badge('Hero', $hero > 0, $hero > 1 ? '(' . $hero . '!)' : '')
. badge('Photos', $photos > 0, (string)($photos ?: ''))
. badge('Teaser', $hasTeaser)
. badge('Description', $hasDescription)
. badge('Highlights', $hasHighlights)
. badge('Resources', (int)$p['resources'] > 0, (string)((int)$p['resources'] ?: ''))
. badge('Datasheets', (int)$p['datasheets'] > 0, (string)((int)$p['datasheets'] ?: ''))
. badge('Solutions', (int)$p['solutions'] > 0, (string)((int)$p['solutions'] ?: ''))
. badge('OG', $og > 0)
. badge('Video', $video > 0)
. '<span class="b lay">' . h((string)$p['resources_layout']) . '</span>'
. implode('', $flags)
. '</div></div></div>' . "\n";
}
// ---- page -------------------------------------------------------------------
$generated = date('Y-m-d H:i');
$statLine = sprintf(
'Hero %d/%d · Photos %d/%d · Teaser %d/%d · Description %d/%d · Highlights %d/%d · Resources %d/%d · Datasheets %d/%d · Solutions %d/%d',
$stat['hero'], $total, $stat['photo'], $total, $stat['teaser'], $total, $stat['description'], $total,
$stat['highlights'], $total, $stat['resources'], $total, $stat['datasheets'], $total, $stat['solutions'], $total
);
$html = '<!doctype html><html lang="en"><head><meta charset="utf-8">'
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
. '<meta name="robots" content="noindex,nofollow">'
. '<title>VITEC Product Audit</title><style>'
. ':root{--ok:#1a7f37;--miss:#c0392b;--line:#e3e5e8;color-scheme:light}'
. 'body{font:14px/1.45 system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c1e21}'
. 'header{position:sticky;top:0;background:#fff;border-bottom:1px solid var(--line);padding:12px 20px;z-index:2}'
. 'h1{font-size:17px;margin:0 0 4px}.stats{color:#555;font-size:12.5px}'
. '.filters{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap}'
. '.filters button{border:1px solid var(--line);background:#fff;border-radius:14px;padding:3px 11px;font-size:12.5px;cursor:pointer}'
. '.filters button.on{background:#26358c;color:#fff;border-color:#26358c}'
. 'main{max-width:1200px;margin:14px auto;padding:0 20px}'
. '.row{display:flex;gap:14px;background:#fff;border:1px solid var(--line);border-radius:8px;margin-bottom:8px;padding:10px;align-items:center}'
. '.row.hide{display:none}'
. '.thumb{flex:0 0 150px;height:84px;display:flex;align-items:center;justify-content:center;background:#fafbfc;border-radius:6px;overflow:hidden}'
. '.thumb img{max-width:100%;max-height:100%;object-fit:contain}'
. '.noimg{color:#b23;font-size:12px}'
. '.t{font-weight:600;margin-bottom:5px}.t a{color:#1c1e21;text-decoration:none}.t a:hover{text-decoration:underline}'
. '.uid{color:#999;font-weight:400;font-size:12px}'
. '.badges{display:flex;flex-wrap:wrap;gap:4px}'
. '.b{font-size:11.5px;border-radius:10px;padding:1px 8px;border:1px solid var(--line)}'
. '.b.ok{background:#e8f5ec;color:var(--ok);border-color:#bfe3c8}'
. '.b.miss{background:#fdecea;color:var(--miss);border-color:#f3c3bd}'
. '.b.flag{background:#fff7e0;color:#8a6d00;border-color:#eadfa8}'
. '.b.lay{background:#eef1fb;color:#26358c;border-color:#ccd4f0}'
. '</style></head><body>'
. '<header><h1>VITEC Product Audit <span class="uid">(' . $total . ' records, generated ' . $generated . ')</span></h1>'
. '<div class="stats">' . h($statLine) . '</div>'
. '<div class="filters">'
. '<button class="on" data-f="">all</button>'
. '<button data-f="f-nohero">no hero</button>'
. '<button data-f="f-nophoto">no photos</button>'
. '<button data-f="f-noteaser">no teaser</button>'
. '<button data-f="f-nohighlights">no highlights</button>'
. '<button data-f="f-legacy">legacy</button>'
. '<button data-f="f-hidden">hidden</button>'
. '</div></header><main>' . "\n" . $rowsHtml . '</main>'
. '<script>document.querySelectorAll(".filters button").forEach(b=>b.addEventListener("click",()=>{'
. 'document.querySelectorAll(".filters button").forEach(x=>x.classList.remove("on"));b.classList.add("on");'
. 'const f=b.dataset.f;document.querySelectorAll(".row").forEach(r=>{r.classList.toggle("hide",f!==""&&!r.classList.contains(f));});'
. '}));</script></body></html>';
$outDir = $root . '/public/product-audit';
if (!is_dir($outDir)) {
mkdir($outDir, 0775, true);
}
file_put_contents($outDir . '/index.html', $html);
echo "geschrieben: public/product-audit/index.html ($total Produkte)\n";
echo "-> https://dev.vitec.com/product-audit/\n";