From 990f27085593cc4f3ef197df7da840b6cc0d1521 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9CVeLiTi=E2=80=9D?= <“info@veliti.nl”>
Date: Wed, 3 Dec 2025 11:30:01 +0100
Subject: [PATCH] Add meta feed functionality and enhance error handling in
catalog API
---
.DS_Store | Bin 12292 -> 12292 bytes
api/v2/get/catalog.php | 66 +++++++++-
assets/functions_meta.php | 256 ++++++++++++++++++++++++++++++++++++++
assets/readdevice.js | 10 +-
assets/scripts.js | 10 +-
firmwaretool.php | 2 +-
6 files changed, 336 insertions(+), 8 deletions(-)
create mode 100644 assets/functions_meta.php
diff --git a/.DS_Store b/.DS_Store
index 15607a8e9066aa8684dc356a6cfde2a19876b481..0b0a16d737eedde9cb1079aed4c86b9e84cd7276 100644
GIT binary patch
delta 119
zcmZokXi1ph&uF_b;4m9!T1s(pQht68<78&`7d#9M42(bw1RUI(4JE}{HmmbIVP@3b
z%p>5;%+Jh_!;sIA4%4#PTX-!GBmZU|1$JhHIh&c4r!#L}AQr^9nN{Np%jO3vyBLAo
GKt=$ANFl%g
delta 89
zcmZokXi1ph&uG0d;4s@{9p#Ta3=9m6Knw&N+?yRG#aTA1^E_c@)Y;4<;LJQpgky7-
o@Is!64Z@6kn|T!2nK$bww=-{EE0)E$nMLC(%Vr_fg^XY>02m<{F#rGn
diff --git a/api/v2/get/catalog.php b/api/v2/get/catalog.php
index 56d1126..97a1b6d 100644
--- a/api/v2/get/catalog.php
+++ b/api/v2/get/catalog.php
@@ -1,6 +1,10 @@
$item['version'],
'config_setting' => $item['config'],
- 'main_option_for_display' => $item['measurement'],
+ 'main_option_for_display' => $item['measurement'] ?? '',
'configurations' => []
];
}
@@ -170,6 +174,64 @@ removeKeysRecursive($catalog,$keys_to_remove);
//------------------------------------------
$messages = processProductCollection($catalog);
+//------------------------------------------
+//check for METAfeed request
+//------------------------------------------
+if (isset($criterias['meta'])){
+ //------------------------------------------
+ // Meta Feed Configuration
+ //------------------------------------------
+ $meta_config = [
+ 'base_url' => 'https://www.morvalwatches.com', // Product page URL
+ 'image_base_url' => 'https://cloud.soveliti.nl',
+ 'brand' => 'Morval Watches',
+ 'currency' => 'EUR',
+ 'condition' => 'new',
+ 'availability' => 'in stock',
+ 'google_product_category' => 'Apparel & Accessories > Jewelry > Watches',
+ 'output_format' => 'json' // Options: 'csv', 'xml', 'json'
+ ];
+
+ //------------------------------------------
+ // Product Code Based Descriptions
+ // Keys can be: exact code (MWTH2NB) or pattern (MWTH1*, MWTH2*)
+ //------------------------------------------
+ $meta_descriptions = [
+ // Pattern based (will match any product starting with this)
+ 'MWTH1' => 'The Thomas-I exudes elegance and sophistication. Classic dimensions combined with subtle details in the dial make it an special automatic watch that can be worn on all occasions.',
+ 'MWTH2' => 'The Thomas-II provides a view of the beating heart of the Swiss timepiece. It marks the precision and perfection with which the time is displayed.',
+ 'MWABR' => 'Handmade Italian calf leather bracelet',
+ ];
+
+ //------------------------------------------
+ //Include meta functions
+ //------------------------------------------
+ include_once './assets/functions_meta.php';
+
+ $meta_feed = catalogToMetaFeed($messages, $meta_config);
+ //------------------------------------------
+ // Output based on format parameter
+ //------------------------------------------
+ if ($criterias['meta'] === true) {
+ $format = $meta_config['output_format'];
+ } else {
+ $format = $criterias['meta'];
+ }
+
+ switch ($format) {
+ case 'xml':
+ $messages = outputMetaFeedXML($meta_feed);
+ break;
+ case 'csv':
+ $messages = outputMetaFeedCSV($meta_feed);
+ break;
+ case 'json':
+ default:
+ $messages = outputMetaFeedJSON($meta_feed);
+ break;
+ }
+ exit();
+}
//------------------------------------------
//JSON_ENCODE
//------------------------------------------
diff --git a/assets/functions_meta.php b/assets/functions_meta.php
new file mode 100644
index 0000000..b8c11a1
--- /dev/null
+++ b/assets/functions_meta.php
@@ -0,0 +1,256 @@
+ $product_id,
+ 'title' => $product_name,
+ 'description' => $description,
+ 'price' => $base_price,
+ 'link' => $product_url,
+ 'image_link' => $main_image,
+ ], $config);
+
+ } else {
+ // Product with variants
+ $variants = extractVariants($product, $base_price);
+
+ foreach ($variants as $variant) {
+ // Combine meta description with variant suffix
+ $variant_description = $description . ' ' . $variant['description_suffix'];
+
+ $meta_feed[] = buildMetaProduct([
+ 'id' => $variant['id'],
+ 'item_group_id' => $product_id,
+ 'title' => $variant['title'],
+ 'description' => $variant_description,
+ 'price' => $variant['price'],
+ 'link' => $product_url,
+ 'image_link' => $variant['image'],
+ 'additional_image_link' => $variant['additional_image'] ?? '',
+ ], $config);
+ }
+ }
+ }
+
+ return $meta_feed;
+}
+
+//------------------------------------------
+// Extract variants from configurations
+//------------------------------------------
+function extractVariants($product, $base_price) {
+ $variants = [];
+ $product_id = $product['productcode'];
+ $product_name = $product['productname'];
+ $image_base_url = $GLOBALS['meta_config']['image_base_url'];
+
+ $configurations = $product['configurations'] ?? [];
+
+ foreach ($configurations as $config) {
+ if ($config['type'] === 'group') {
+ $group_name = $config['assignment_name'] ?? 'Option';
+ $attributes = $config['attributes'] ?? [];
+
+ foreach ($attributes as $attr) {
+ // Calculate price
+ $attr_price = floatval($attr['price'] ?? 0);
+ $attr_modifier = intval($attr['price_modifier'] ?? 1);
+ $final_price = calculateVariantPrice($base_price, $attr_price, $attr_modifier);
+
+ // Get images (using image_base_url for images)
+ $variant_image = buildImageUrl($attr['alternative_media_full_path'] ?? '', $image_base_url);
+ $additional_image = buildImageUrl($attr['full_path'] ?? '', $image_base_url);
+
+ // Fallback to main product image if no variant image
+ if (empty($variant_image)) {
+ $variant_image = buildImageUrl($product['full_path'] ?? '', $image_base_url);
+ }
+
+ // Extract option name from item_name
+ $item_name = $attr['item_name'] ?? '';
+ $option_name = formatAttributeName($item_name);
+
+ // Build variant
+ $variants[] = [
+ 'id' => $product_id . '_' . ($attr['attribute_id'] ?? ''),
+ 'title' => $product_name . ' - ' . $option_name,
+ 'description_suffix' => 'Available with ' . $option_name . ' ' . strtolower($group_name) . '.',
+ 'price' => $final_price,
+ 'image' => $variant_image,
+ 'additional_image' => $additional_image,
+ ];
+ }
+ }
+ }
+
+ return $variants;
+}
+
+//------------------------------------------
+// Build Meta product row
+//------------------------------------------
+function buildMetaProduct($data, $config) {
+ $product = [
+ 'id' => $data['id'],
+ 'title' => $data['title'],
+ 'description' => $data['description'],
+ 'availability' => $config['availability'],
+ 'condition' => $config['condition'],
+ 'price' => formatPrice($data['price'], $config['currency']),
+ 'link' => $data['link'],
+ 'image_link' => $data['image_link'],
+ 'brand' => $config['brand'],
+ 'google_product_category' => $config['google_product_category'],
+ ];
+
+ // Add optional fields if present
+ if (!empty($data['item_group_id'])) {
+ $product['item_group_id'] = $data['item_group_id'];
+ }
+
+ if (!empty($data['additional_image_link'])) {
+ $product['additional_image_link'] = $data['additional_image_link'];
+ }
+
+ return $product;
+}
+
+//------------------------------------------
+// Helper Functions
+//------------------------------------------
+function getMetaDescription($product_code, $fallback_description = '') {
+ global $meta_descriptions;
+
+ // First check for exact match
+ if (isset($meta_descriptions[$product_code])) {
+ return $meta_descriptions[$product_code];
+ }
+
+ // Then check for pattern match (starts with)
+ foreach ($meta_descriptions as $pattern => $description) {
+ if (strpos($product_code, $pattern) === 0) {
+ return $description;
+ }
+ }
+
+ // Return fallback if no match
+ return $fallback_description;
+}
+
+function calculateVariantPrice($base_price, $modifier_price, $modifier_type) {
+ if ($modifier_price <= 0) {
+ return $base_price;
+ }
+
+ switch ($modifier_type) {
+ case 1: // Addition
+ return $base_price + $modifier_price;
+ case 2: // Multiplication
+ return $base_price * $modifier_price;
+ default:
+ return $base_price;
+ }
+}
+
+function buildImageUrl($path, $base_url) {
+ if (empty($path)) return '';
+ return rtrim($base_url, '/') . '/' . ltrim($path, '/');
+}
+
+function buildProductUrl($slug, $base_url) {
+ if (empty($slug)) return '';
+ return rtrim($base_url, '/') . '/product/' . $slug;
+}
+
+function formatPrice($price, $currency) {
+ return number_format($price, 2, '.', '') . ' ' . $currency;
+}
+
+function cleanText($text) {
+ $text = strip_tags($text);
+ $text = preg_replace('/\s+/', ' ', $text);
+ return trim($text);
+}
+
+function formatAttributeName($name) {
+ // Remove prefix like "bracelet_"
+ $name = preg_replace('/^bracelet_/', '', $name);
+ // Replace underscores with spaces
+ $name = str_replace('_', ' ', $name);
+ // Capitalize words
+ return ucwords($name);
+}
+
+//------------------------------------------
+// Output Functions
+//------------------------------------------
+function outputMetaFeedCSV($meta_feed) {
+ header('Content-Type: text/csv; charset=utf-8');
+ header('Content-Disposition: attachment; filename="meta_product_feed.csv"');
+
+ $output = fopen('php://output', 'w');
+
+ // Write headers
+ if (!empty($meta_feed)) {
+ fputcsv($output, array_keys($meta_feed[0]));
+
+ // Write data
+ foreach ($meta_feed as $row) {
+ fputcsv($output, $row);
+ }
+ }
+
+ fclose($output);
+}
+
+function outputMetaFeedXML($meta_feed) {
+ header('Content-Type: application/xml; charset=utf-8');
+
+ $xml = new SimpleXMLElement('');
+ $channel = $xml->addChild('channel');
+ $channel->addChild('title', 'Morval Product Feed');
+ $channel->addChild('link', $GLOBALS['meta_config']['base_url']);
+ $channel->addChild('description', 'Product feed for Meta catalog');
+
+ foreach ($meta_feed as $product) {
+ $item = $channel->addChild('item');
+
+ foreach ($product as $key => $value) {
+ $xml_key = 'g:' . $key;
+ $item->addChild($xml_key, htmlspecialchars($value));
+ }
+ }
+
+ echo $xml->asXML();
+}
+
+function outputMetaFeedJSON($meta_feed) {
+ //header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($meta_feed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+}
+
+?>
\ No newline at end of file
diff --git a/assets/readdevice.js b/assets/readdevice.js
index 379b221..8e19e5c 100644
--- a/assets/readdevice.js
+++ b/assets/readdevice.js
@@ -1,7 +1,7 @@
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
const lineHistory = [];
-maintenanceRun = 0;
-handshakeComplete = false;
+let maintenanceRun = 0;
+let handshakeComplete = false;
// Function to log communication to API
async function logCommunication(data, direction) {
@@ -102,6 +102,7 @@ async function connectSerial() {
// Log connection failure details
await logCommunication(`Serial connection failed: ${error.message || 'Unknown error'}`, 'disconnected');
alert("Serial Connection Failed");
+ window.location.reload();
}
}
@@ -580,7 +581,7 @@ async function closePort(){
maintenanceRun = 0; // reset maintenanceRun
// Refresh the page
- window.location.reload();
+ setTimeout(() => {window.location.reload();}, 500);
}
async function closePortCarTest(){
@@ -736,7 +737,7 @@ async function updateHistory(){
body: testdetails
});
- const historyresult = await response.json;
+ const historyresult = await response.json();
setTimeout(maintenanceTest,5000);
@@ -805,5 +806,6 @@ async function updateHistory(){
catch(e)
{
alert(e.message || e);
+ window.location.reload();
}
}
diff --git a/assets/scripts.js b/assets/scripts.js
index 12d0961..8cb1139 100644
--- a/assets/scripts.js
+++ b/assets/scripts.js
@@ -1,5 +1,10 @@
const serialResultsDiv = document.getElementById("serialResults");
+let port, textEncoder, writableStreamClosed, writer;
+let readableStreamClosed, reader, openPort = 0;
+let x, serial, fw, hw, sw, commitCode;
+let firmwarelocation, upgraded_version, hex_fw;
+
// Buffer for accumulating received data before logging
let receivedDataBuffer = '';
@@ -113,13 +118,15 @@ async function connectDevice() {
// Log connection failure details
await logCommunication(`Serial connection failed: ${error.message || 'Unknown error'}`, 'disconnected');
- if (openPort = 1){
+ if (openPort === 1){
closePort();
console.log("Closing port");
alert("System is still trying to close the serial port. If this message continues to come up please refresh this page.");
+ window.location.reload();
}
else{
alert("Your browser does not support this functionality. Please use latest Chrome or Edge browser.");
+ window.location.reload();
}
}
}
@@ -161,6 +168,7 @@ async function listenToPort() {
async function appendToTerminal(newStuff) {
serialResultsDiv.innerHTML += newStuff;
+ receivedDataBuffer += newStuff;
// Log received data
await logCommunication(newStuff.trim(), 'received');
diff --git a/firmwaretool.php b/firmwaretool.php
index 835017e..81d81d9 100644
--- a/firmwaretool.php
+++ b/firmwaretool.php
@@ -100,7 +100,7 @@ echo '
';