From 000c054734669ffcee603448af1f1c3d19d38dbe Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Thu, 19 Jun 2025 15:46:22 -0400 Subject: [PATCH 01/11] fixing field edits and adding custom fileds --- src/app.py | 64 +++-- src/database.py | 156 +++++++++++- src/routes/admin.py | 346 ++++++++++++++++++++------ src/templates/admin/add_product.html | 60 +++-- src/templates/admin/ar_fields.html | 170 +++++++++++++ src/templates/admin/edit_product.html | 114 +++++---- src/templates/admin/index.html | 1 + src/templates/index.html | 61 +++-- 8 files changed, 792 insertions(+), 180 deletions(-) create mode 100644 src/templates/admin/ar_fields.html diff --git a/src/app.py b/src/app.py index b172660..1ab0ce9 100644 --- a/src/app.py +++ b/src/app.py @@ -76,12 +76,8 @@ def login(tenant_id): @app.route('//arcontentfields', methods=['GET']) def get_ar_content_fields(tenant_id): - # Return a fixed set of attributes (tenant_id could be used for custom fields in the future) - fields = [ - {"fieldName": "_id", "label": "Item ID", "editable": "false", "fieldType": "TEXT"}, - {"fieldName": "_price", "label": "Sale Price", "editable": "true", "fieldType": "TEXT"}, - {"fieldName": "_image", "label": "Image", "editable": "false", "fieldType": "IMAGE_URI"} - ] + # Get custom fields defined for this tenant + fields = database.get_custom_ar_fields(tenant_id) return jsonify(fields), 200 @app.route('//arinfo', methods=['GET']) @@ -89,31 +85,39 @@ def get_ar_info(tenant_id): barcode = request.args.get('barcode') products = load_products(tenant_id) - # Helper function to convert relative image paths to absolute URLs - def make_absolute_urls(product_fields): - # Create absolute URL for image fields + # Get custom AR fields for this tenant + custom_fields = database.get_custom_ar_fields(tenant_id) + custom_field_names = [f['fieldName'] for f in custom_fields] + + # Helper function to convert relative image paths to absolute URLs and filter fields + def filter_and_process_fields(product_fields): + # Filter to only include fields defined in custom AR fields + filtered_fields = [] for field in product_fields: - if field['fieldName'] == '_image' and field['value']: - # If it's already an absolute URL, leave it as is - if not field['value'].startswith('http'): - # Build absolute URL using request host with tenant - field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" - return product_fields + if field['fieldName'] in custom_field_names: + # Create absolute URL for image fields + if field['fieldName'] == '_image' and field['value']: + # If it's already an absolute URL, leave it as is + if not field['value'].startswith('http'): + # Build absolute URL using request host with tenant + field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" + filtered_fields.append(field) + return filtered_fields # If barcode is provided, return specific product if barcode: if barcode in products: - product_data = make_absolute_urls(products[barcode]) + product_data = filter_and_process_fields(products[barcode]) response = jsonify(product_data) response.headers['Access-Control-Allow-Origin'] = '*' return response, 200 return jsonify({"error": "Product not found"}), 404 # Return all products if no barcode specified - # Convert all products to have absolute URLs + # Convert all products to have absolute URLs and filter fields all_products = {} for product_id, fields in products.items(): - all_products[product_id] = make_absolute_urls(fields) + all_products[product_id] = filter_and_process_fields(fields) response = jsonify(all_products) response.headers['Access-Control-Allow-Origin'] = '*' return response, 200 @@ -123,7 +127,26 @@ def serve_image(tenant_id, filename): # Log the request for debugging app.logger.info(f"Image requested for tenant {tenant_id}: {filename}") - # Extract product ID from filename (e.g., "123456.jpg" -> "123456") + # Check if filename contains field name (e.g., "123456_thumbnail.jpg") + base_name = os.path.splitext(filename)[0] + + if '_' in base_name: + # Split to get product_id and field_name + parts = base_name.split('_', 1) + product_id = parts[0] + field_name = '_' + parts[1] if len(parts) > 1 else '_image' + + # Try to get field-specific image + image_data = database.get_product_image_by_field(product_id, tenant_id, field_name) + if image_data: + image_bytes, mime_type = image_data + response = Response(image_bytes, mimetype=mime_type) + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Cache-Control'] = 'public, max-age=3600' + app.logger.info(f"Serving field-specific image: {product_id}/{field_name} ({len(image_bytes)} bytes)") + return response + + # Try standard image lookup (backward compatibility) product_id = os.path.splitext(filename)[0] # Get image from database for this tenant @@ -203,7 +226,7 @@ def serve_barcode(tenant_id, filename): # Import the admin routes directly and register them with tenant support from routes.admin import (index as admin_index, add_product, edit_product, delete_product, view_product, generate_barcode, - generate_barcode_page, manage_credentials) + generate_barcode_page, manage_credentials, manage_ar_fields) # Register admin routes with tenant prefix app.add_url_rule('//admin/', 'admin.index', admin_index) @@ -214,6 +237,7 @@ app.add_url_rule('//admin/view/', 'admin.view_prod app.add_url_rule('//admin/generate_barcode//', 'admin.generate_barcode', generate_barcode) app.add_url_rule('//admin/generate_barcode_page/', 'admin.generate_barcode_page', generate_barcode_page) app.add_url_rule('//admin/credentials', 'admin.manage_credentials', manage_credentials, methods=['GET', 'POST']) +app.add_url_rule('//admin/ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST']) # Register API routes with tenant prefix from routes.api import api_index diff --git a/src/database.py b/src/database.py index 81f530f..d8692ec 100644 --- a/src/database.py +++ b/src/database.py @@ -75,6 +75,38 @@ def init_database(): ) ''') + # Create custom_ar_fields table for tenant-specific AR field definitions + cursor.execute(''' + CREATE TABLE IF NOT EXISTS custom_ar_fields ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tenant_id TEXT NOT NULL, + field_name TEXT NOT NULL, + label TEXT NOT NULL, + field_type TEXT NOT NULL, + editable TEXT DEFAULT 'true', + display_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + UNIQUE(tenant_id, field_name) + ) + ''') + + # Create product_images table for storing multiple images per product + cursor.execute(''' + CREATE TABLE IF NOT EXISTS product_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + field_name TEXT NOT NULL, + image_data BLOB, + image_mime_type TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id, tenant_id) REFERENCES products(id, tenant_id) ON DELETE CASCADE, + UNIQUE(product_id, tenant_id, field_name) + ) + ''') + conn.commit() def migrate_from_json(): @@ -340,6 +372,9 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N ''', (tenant_id, tenant_id.title(), default_username, default_password)) conn.commit() + # Initialize default AR fields for the new tenant + init_default_ar_fields(tenant_id) + return { 'id': tenant_id, 'name': tenant_id.title(), @@ -400,4 +435,123 @@ def cleanup_reserved_tenants(): print(f"Deleted reserved tenant: {tenant_id}") conn.commit() - return deleted_count \ No newline at end of file + return deleted_count + +def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]: + """Get custom AR fields for a tenant""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT id, field_name, label, field_type, editable, display_order + FROM custom_ar_fields + WHERE tenant_id = ? + ORDER BY display_order, field_name + ''', (tenant_id,)) + + fields = [] + for row in cursor.fetchall(): + fields.append({ + 'id': row['id'], + 'fieldName': row['field_name'], + 'label': row['label'], + 'fieldType': row['field_type'], + 'editable': row['editable'], + 'displayOrder': row['display_order'] + }) + + return fields + +def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int: + """Save or update a custom AR field""" + with get_db() as conn: + cursor = conn.cursor() + + if 'id' in field_data: + # Update existing field + cursor.execute(''' + UPDATE custom_ar_fields + SET field_name = ?, label = ?, field_type = ?, editable = ?, + display_order = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND tenant_id = ? + ''', ( + field_data['fieldName'], + field_data['label'], + field_data['fieldType'], + field_data.get('editable', 'true'), + field_data.get('displayOrder', 0), + field_data['id'], + tenant_id + )) + conn.commit() + return field_data['id'] + else: + # Insert new field + cursor.execute(''' + INSERT INTO custom_ar_fields + (tenant_id, field_name, label, field_type, editable, display_order) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + tenant_id, + field_data['fieldName'], + field_data['label'], + field_data['fieldType'], + field_data.get('editable', 'true'), + field_data.get('displayOrder', 0) + )) + conn.commit() + return cursor.lastrowid + +def delete_custom_ar_field(tenant_id: str, field_id: int): + """Delete a custom AR field""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM custom_ar_fields + WHERE id = ? AND tenant_id = ? + ''', (field_id, tenant_id)) + conn.commit() + +def init_default_ar_fields(tenant_id: str): + """Initialize default AR fields for a new tenant""" + default_fields = [ + {"fieldName": "_id", "label": "Item ID", "fieldType": "TEXT", "editable": "false", "displayOrder": 1}, + {"fieldName": "_price", "label": "Sale Price", "fieldType": "TEXT", "editable": "true", "displayOrder": 2}, + {"fieldName": "_image", "label": "Image", "fieldType": "IMAGE_URI", "editable": "false", "displayOrder": 3} + ] + + for field in default_fields: + save_custom_ar_field(tenant_id, field) + +def save_product_image(product_id: str, tenant_id: str, field_name: str, image_data: bytes, mime_type: str): + """Save an image for a specific field of a product""" + with get_db() as conn: + cursor = conn.cursor() + + # Delete existing image for this field if any + cursor.execute(''' + DELETE FROM product_images + WHERE product_id = ? AND tenant_id = ? AND field_name = ? + ''', (product_id, tenant_id, field_name)) + + # Insert new image + cursor.execute(''' + INSERT INTO product_images (product_id, tenant_id, field_name, image_data, image_mime_type) + VALUES (?, ?, ?, ?, ?) + ''', (product_id, tenant_id, field_name, image_data, mime_type)) + + conn.commit() + +def get_product_image_by_field(product_id: str, tenant_id: str, field_name: str) -> Optional[tuple[bytes, str]]: + """Get image data for a specific field of a product""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT image_data, image_mime_type + FROM product_images + WHERE product_id = ? AND tenant_id = ? AND field_name = ? + ''', (product_id, tenant_id, field_name)) + + row = cursor.fetchone() + if row and row['image_data']: + return row['image_data'], row['image_mime_type'] + return None \ No newline at end of file diff --git a/src/routes/admin.py b/src/routes/admin.py index cf49501..7bb087e 100644 --- a/src/routes/admin.py +++ b/src/routes/admin.py @@ -41,119 +41,264 @@ def index(tenant_id): return render_template('admin/index.html', products=products, tenant=tenant) def add_product(tenant_id): + # Get custom AR fields for this tenant + custom_fields = database.get_custom_ar_fields(tenant_id) + if request.method == 'POST': # Get form data product_id = request.form.get('product_id') - name = request.form.get('name') - price = request.form.get('price') # Basic validation - if not product_id or not name or not price: - flash('Product ID, Name, and Price are required fields.') - return render_template('admin/add_product.html', tenant_id=tenant_id) + if not product_id: + flash('Product ID is required.') + return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields) # Check if product ID already exists products = load_products(tenant_id) if product_id in products: flash('Product ID already exists.') - return render_template('admin/add_product.html', tenant_id=tenant_id) + return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields) - # Handle image upload + # Handle backward compatibility image upload image_data = None image_mime_type = None - image_path = f"/images/{product_id}.png" # Default path for display - if 'image' in request.files: - file = request.files['image'] - if file and file.filename and allowed_file(file.filename): - extension = file.filename.rsplit('.', 1)[1].lower() - image_path = f"/images/{product_id}.{extension}" - - # Read image data - image_data = file.read() - - # Determine mime type - mime_types = { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif' - } - image_mime_type = mime_types.get(extension, 'image/jpeg') + # Create product data structure based on custom fields + product_data = [] - # Create product data structure - product_data = [ - {"fieldName": "_id", "label": "Item ID", "value": product_id, "editable": "false", "fieldType": "TEXT"}, - {"fieldName": "_name", "label": "Product Name", "value": name, "editable": "true", "fieldType": "TEXT"}, - {"fieldName": "_price", "label": "Sale Price", "value": f"${price}", "editable": "true", "fieldType": "TEXT"}, - {"fieldName": "_image", "label": "Image", "value": image_path, "editable": "false", "fieldType": "IMAGE_URI"} - ] + # Always include _id field + product_data.append({ + "fieldName": "_id", + "label": "Item ID", + "value": product_id, + "editable": "false", + "fieldType": "TEXT" + }) + + # Process images after product is saved + images_to_save = [] + + # Add other fields based on custom configuration + for field in custom_fields: + field_name = field['fieldName'] + if field_name == '_id': + continue # Already added + elif field['fieldType'] == 'IMAGE_URI': + # Check if image was uploaded for this field + image_field_name = f'image_{field_name}' + if image_field_name in request.files: + file = request.files[image_field_name] + if file and file.filename and allowed_file(file.filename): + # Read image data + file.seek(0) + img_data = file.read() + + # Determine mime type + extension = file.filename.rsplit('.', 1)[1].lower() + mime_types = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif' + } + mime_type = mime_types.get(extension, 'image/jpeg') + + # Store for later saving + images_to_save.append({ + 'field_name': field_name, + 'data': img_data, + 'mime_type': mime_type + }) + + # Set the image URL path + image_path = f"/images/{product_id}_{field_name}.{extension}" + + # For backward compatibility with _image field + if field_name == '_image': + image_data = img_data + image_mime_type = mime_type + else: + image_path = "" + else: + image_path = "" + + product_data.append({ + "fieldName": field_name, + "label": field['label'], + "value": image_path, + "editable": field['editable'], + "fieldType": field['fieldType'] + }) + else: + # Get value from form + value = request.form.get(f'field_{field_name}', '') + product_data.append({ + "fieldName": field_name, + "label": field['label'], + "value": value, + "editable": field['editable'], + "fieldType": field['fieldType'] + }) + + # Also save fields that aren't in custom fields (for backward compatibility) + if '_name' not in [f['fieldName'] for f in custom_fields]: + name = request.form.get('name', '') + if name: + product_data.append({ + "fieldName": "_name", + "label": "Product Name", + "value": name, + "editable": "true", + "fieldType": "TEXT" + }) + + if '_price' not in [f['fieldName'] for f in custom_fields]: + price = request.form.get('price', '') + if price: + product_data.append({ + "fieldName": "_price", + "label": "Sale Price", + "value": f"${price}", + "editable": "true", + "fieldType": "TEXT" + }) # Save to database database.save_product(product_id, tenant_id, product_data, image_data, image_mime_type) + # Save additional images + for img in images_to_save: + database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) + flash('Product added successfully!') return redirect(url_for('admin.index', tenant_id=tenant_id)) - return render_template('admin/add_product.html', tenant_id=tenant_id) + return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields) def edit_product(tenant_id, product_id): products = load_products(tenant_id) + custom_fields = database.get_custom_ar_fields(tenant_id) if product_id not in products: flash('Product not found.') return redirect(url_for('admin.index', tenant_id=tenant_id)) if request.method == 'POST': - # Get form data - name = request.form.get('name') - price = request.form.get('price') - - # Basic validation - if not name or not price: - flash('Name and Price are required fields.') - return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) - - # Update name and price - for field in products[product_id]: - if field["fieldName"] == "_name": - field["value"] = name - elif field["fieldName"] == "_price": - field["value"] = f"${price}" - - # Handle image upload + # Handle backward compatibility image upload image_data = None image_mime_type = None - if 'image' in request.files and request.files['image'].filename: - file = request.files['image'] - if allowed_file(file.filename): - # Read new image data - image_data = file.read() + # Process images after product is saved + images_to_save = [] + + # Update fields based on form data + updated_product = [] + + # Always include _id field + for field in products[product_id]: + if field["fieldName"] == "_id": + updated_product.append(field) + break + + # Process each custom field + for custom_field in custom_fields: + field_name = custom_field['fieldName'] + if field_name == '_id': + continue - # Determine mime type - extension = file.filename.rsplit('.', 1)[1].lower() - mime_types = { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif' + # Find existing field data + existing_field = None + for field in products[product_id]: + if field["fieldName"] == field_name: + existing_field = field.copy() + break + + if not existing_field: + existing_field = { + "fieldName": field_name, + "label": custom_field['label'], + "value": "", + "editable": custom_field['editable'], + "fieldType": custom_field['fieldType'] } - image_mime_type = mime_types.get(extension, 'image/jpeg') - - # Update image path in product data - image_path = f"/images/{product_id}.{extension}" + + if custom_field['fieldType'] == 'IMAGE_URI': + # Check if new image was uploaded + image_field_name = f'image_{field_name}' + if image_field_name in request.files and request.files[image_field_name].filename: + file = request.files[image_field_name] + if allowed_file(file.filename): + # Read image data + file.seek(0) + img_data = file.read() + + # Determine mime type + extension = file.filename.rsplit('.', 1)[1].lower() + mime_types = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif' + } + mime_type = mime_types.get(extension, 'image/jpeg') + + # Store for later saving + images_to_save.append({ + 'field_name': field_name, + 'data': img_data, + 'mime_type': mime_type + }) + + # Update image path + existing_field["value"] = f"/images/{product_id}_{field_name}.{extension}" + + # For backward compatibility with _image field + if field_name == '_image': + image_data = img_data + image_mime_type = mime_type + else: + # Get value from form + value = request.form.get(f'field_{field_name}', '') + if field_name == '_price' and value and not value.startswith('$'): + value = f"${value}" + existing_field["value"] = value + + updated_product.append(existing_field) + + # Handle backward compatibility fields + if '_name' not in [f['fieldName'] for f in custom_fields]: + name = request.form.get('name', '') + if name: for field in products[product_id]: - if field["fieldName"] == "_image": - field["value"] = image_path + if field["fieldName"] == "_name": + field["value"] = name + updated_product.append(field) + break + + if '_price' not in [f['fieldName'] for f in custom_fields]: + price = request.form.get('price', '') + if price: + for field in products[product_id]: + if field["fieldName"] == "_price": + field["value"] = f"${price}" + updated_product.append(field) + break # Save to database - database.save_product(product_id, tenant_id, products[product_id], image_data, image_mime_type) + database.save_product(product_id, tenant_id, updated_product, image_data, image_mime_type) + + # Save additional images + for img in images_to_save: + database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) flash('Product updated successfully!') return redirect(url_for('admin.index', tenant_id=tenant_id)) - return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) + return render_template('admin/edit_product.html', + product_id=product_id, + product=products[product_id], + tenant_id=tenant_id, + custom_fields=custom_fields) def delete_product(tenant_id, product_id): products = load_products(tenant_id) @@ -245,3 +390,64 @@ def manage_credentials(tenant_id): flash('Username and password are required.') return render_template('admin/credentials.html', tenant=tenant, tenant_id=tenant_id) + +def manage_ar_fields(tenant_id): + """Manage custom AR content fields""" + tenant = database.get_or_create_tenant(tenant_id) + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'add': + field_data = { + 'fieldName': request.form.get('fieldName'), + 'label': request.form.get('label'), + 'fieldType': request.form.get('fieldType'), + 'editable': request.form.get('editable', 'true'), + 'displayOrder': int(request.form.get('displayOrder', 0)) + } + + if field_data['fieldName'] and field_data['label']: + database.save_custom_ar_field(tenant_id, field_data) + flash('Custom field added successfully!') + else: + flash('Field name and label are required.') + + elif action == 'delete': + field_id = request.form.get('field_id') + if field_id: + database.delete_custom_ar_field(tenant_id, int(field_id)) + flash('Custom field deleted successfully!') + + elif action == 'update': + field_data = { + 'id': int(request.form.get('field_id')), + 'fieldName': request.form.get('fieldName'), + 'label': request.form.get('label'), + 'fieldType': request.form.get('fieldType'), + 'editable': request.form.get('editable', 'true'), + 'displayOrder': int(request.form.get('displayOrder', 0)) + } + + if field_data['fieldName'] and field_data['label']: + database.save_custom_ar_field(tenant_id, field_data) + flash('Custom field updated successfully!') + else: + flash('Field name and label are required.') + + return redirect(url_for('admin.manage_ar_fields', tenant_id=tenant_id)) + + # Get existing custom fields + custom_fields = database.get_custom_ar_fields(tenant_id) + + # Available field types + field_types = [ + 'TEXT', + 'IMAGE_URI' + ] + + return render_template('admin/ar_fields.html', + tenant=tenant, + tenant_id=tenant_id, + custom_fields=custom_fields, + field_types=field_types) diff --git a/src/templates/admin/add_product.html b/src/templates/admin/add_product.html index 2efc1fa..099495a 100644 --- a/src/templates/admin/add_product.html +++ b/src/templates/admin/add_product.html @@ -22,29 +22,49 @@
Must be unique. This will be used as the barcode for AR content.
-
- - -
Enter a descriptive name for the product.
-
+ {% for field in custom_fields %} + {% if field.fieldName != '_id' %} +
+ + {% if field.fieldType == 'IMAGE_URI' %} + +
Supported formats: .png, .jpg, .jpeg, .gif
+
+ +
+ {% elif field.fieldType == 'TEXT' %} + {% if field.fieldName == '_price' %} +
+ $ + +
+ {% else %} + + {% endif %} + {% else %} + + {% endif %} +
+ {% endif %} + {% endfor %} + + {% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %}
- + + +
+ {% endif %} + + {% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %} +
+
$ - -
-
Enter the price in decimal format (e.g., 49.99)
-
- -
- - -
Supported formats: .png, .jpg, .jpeg, .gif
-
- +
+ {% endif %}
Cancel @@ -59,8 +79,8 @@ {% block extra_js %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/ar_fields.html b/src/templates/admin/ar_fields.html new file mode 100644 index 0000000..b49f93b --- /dev/null +++ b/src/templates/admin/ar_fields.html @@ -0,0 +1,170 @@ +{% extends "layout.html" %} + +{% block title %}Manage AR Fields - KCAP Admin{% endblock %} + +{% block content %} +
+
+

Manage AR Content Fields

+

Configure the fields that will be returned by the /arcontentfields and /arinfo endpoints.

+
+ Back to Admin +
+ +
+
+
Current AR Fields
+
+
+ {% if custom_fields %} + + + + + + + + + + + + + {% for field in custom_fields %} + + + + + + + + + {% endfor %} + +
Field NameLabelTypeEditableDisplay OrderActions
{{ field.fieldName }}{{ field.label }}{{ field.fieldType }}{{ field.editable }}{{ field.displayOrder }} + +
+ + + +
+
+ {% else %} +

No custom fields defined. Default fields will be used.

+ {% endif %} +
+
+ +
+
+
Add/Edit AR Field
+
+
+
+ + + +
+ + + Use underscore prefix for system fields (e.g., _id, _price) + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/edit_product.html b/src/templates/admin/edit_product.html index f085b0e..e7f85ad 100644 --- a/src/templates/admin/edit_product.html +++ b/src/templates/admin/edit_product.html @@ -17,52 +17,80 @@
Product ID cannot be changed.
-
- + {% for field in custom_fields %} + {% if field.fieldName != '_id' %} +
+ + + {% set current_value = '' %} + {% for prod_field in product %} + {% if prod_field.fieldName == field.fieldName %} + {% set current_value = prod_field.value %} + {% endif %} + {% endfor %} + + {% if field.fieldType == 'IMAGE_URI' %} + +
Leave empty to keep the current image.
+ + {% if current_value %} +
+ +
+ Current image +
+
+ {% endif %} + +
+ + +
+ {% elif field.fieldType == 'TEXT' %} + {% if field.fieldName == '_price' %} +
+ $ + {% set price_value = current_value|replace('$', '') %} + +
+ {% else %} + + {% endif %} + {% else %} + + {% endif %} +
+ {% endif %} + {% endfor %} + + + {% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %} {% for field in product %} {% if field.fieldName == '_name' %} - +
+ + +
{% endif %} {% endfor %} -
Enter a descriptive name for the product.
-
+ {% endif %} -
- -
- $ - {% for field in product %} - {% if field.fieldName == '_price' %} + {% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %} + {% for field in product %} + {% if field.fieldName == '_price' %} +
+ +
+ $ {% set price_value = field.value|replace('$', '') %} - - {% endif %} - {% endfor %} -
-
Enter the price in decimal format (e.g., 49.99)
-
- -
- - -
Leave empty to keep the current image.
- -
- - {% for field in product %} - {% if field.fieldName == '_image' %} -
- Current product image -
- {% endif %} - {% endfor %} -
- -
- - -
-
+ +
+
+ {% endif %} + {% endfor %} + {% endif %}
Cancel @@ -77,8 +105,8 @@ {% block extra_js %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index 52079f8..4371b28 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -9,6 +9,7 @@ Current Tenant: {{ tenant.name }} (ID: {{ tenant.id }})
diff --git a/src/templates/index.html b/src/templates/index.html index 86cc56a..b20730a 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -7,36 +7,45 @@

KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}

-

A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.

+

Manage your product catalog and AR content for barcode scanning applications.


-

This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.

+
-

API Endpoints

-
    -
  • - Login: GET {% if tenant %}/{{ tenant.id }}{% endif %}/login -

    Authenticates using Basic Auth with tenant-specific credentials.

    -
  • -
  • - Content Fields: GET {% if tenant %}/{{ tenant.id }}{% endif %}/arcontentfields -

    Returns a list of available attributes (e.g., item ID, price, image URI).

    -
  • -
  • - AR Info: GET {% if tenant %}/{{ tenant.id }}{% endif %}/arinfo?barcode=123456 -

    Returns product details for a given barcode, including image URLs.

    -
  • -
  • - Static Image Server: GET {% if tenant %}/{{ tenant.id }}{% endif %}/images/123456.png -

    Serves product images stored in the database.

    -
  • -
+

Quick Actions

+
+
+
+
+
Product Management
+

Add, edit, and manage your product catalog with custom fields.

+ {% if tenant %} + Manage Products + {% endif %} +
+
+
+
+
+
+
AR Field Configuration
+

Customize the fields returned by AR content endpoints.

+ {% if tenant %} + Configure AR Fields + {% endif %} +
+
+
+
+
- {% if tenant %} - Go to Admin Interface - {% else %} - Go to Admin Interface - {% endif %} +

Getting Started

+
    +
  1. Configure your AR fields to define what information is returned for products
  2. +
  3. Add products to your catalog with the configured fields
  4. +
  5. Generate barcodes for your products
  6. +
  7. Use the AR endpoints in your scanning application
  8. +
From e744afbe26c66f5878ee2e630db6789e0f55a556 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Thu, 19 Jun 2025 16:03:50 -0400 Subject: [PATCH 02/11] adding server settings --- src/app.py | 23 ++++++++-- src/database.py | 70 +++++++++++++++++++++++++++-- src/templates/settings.html | 52 +++++++++++++++++++++ src/templates/tenant_selection.html | 2 + 4 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 src/templates/settings.html diff --git a/src/app.py b/src/app.py index 1ab0ce9..f94377d 100644 --- a/src/app.py +++ b/src/app.py @@ -28,7 +28,8 @@ def load_products(tenant_id=None): def index(): # Show a tenant selection page or redirect to default tenants = database.get_all_tenants() - return render_template('tenant_selection.html', tenants=tenants) + server_url = database.get_server_url() + return render_template('tenant_selection.html', tenants=tenants, server_url=server_url) @app.route('/tenant//delete', methods=['POST']) def delete_tenant(tenant_id): @@ -37,6 +38,22 @@ def delete_tenant(tenant_id): flash(f'Tenant "{tenant_id}" has been deleted successfully.', 'success') return redirect('/') +@app.route('/settings', methods=['GET', 'POST']) +def settings(): + if request.method == 'POST': + server_url = request.form.get('server_url', '').strip() + if server_url: + # Remove trailing slash for consistency + server_url = server_url.rstrip('/') + database.set_setting('server_url', server_url) + flash('Server settings updated successfully.', 'success') + else: + flash('Please provide a valid server URL.', 'error') + return redirect('/settings') + + server_url = database.get_server_url() + return render_template('settings.html', server_url=server_url) + @app.route('//') def tenant_index(tenant_id): # Auto-create tenant if it doesn't exist @@ -56,8 +73,8 @@ def check_basic_auth(auth_header, tenant_id): credentials = base64.b64decode(auth_header[6:]).decode('utf-8') username, password = credentials.split(':', 1) - # Get tenant credentials from database - tenant = database.get_or_create_tenant(tenant_id) + # Get tenant credentials from database (without creating) + tenant = database.get_tenant(tenant_id) if tenant and username == tenant['username'] and password == tenant['password']: return True except Exception: diff --git a/src/database.py b/src/database.py index d8692ec..836f9a6 100644 --- a/src/database.py +++ b/src/database.py @@ -107,6 +107,16 @@ def init_database(): ) ''') + # Create settings table for server configuration + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() def migrate_from_json(): @@ -342,10 +352,34 @@ def get_product_image(product_id: str, tenant_id: str) -> Optional[tuple[bytes, return row['image_data'], row['image_mime_type'] return None +def get_tenant(tenant_id: str) -> Optional[Dict[str, Any]]: + """Get a tenant without creating it""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + + with get_db() as conn: + cursor = conn.cursor() + + # Check if tenant exists + cursor.execute('SELECT id, name, username, password FROM tenants WHERE id = ?', (tenant_id,)) + row = cursor.fetchone() + + if row: + return { + 'id': row['id'], + 'name': row['name'], + 'username': row['username'], + 'password': row['password'] + } + return None + def get_or_create_tenant(tenant_id: str, username: str = None, password: str = None) -> Optional[Dict[str, Any]]: """Get or create a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + # Check if tenant_id is reserved - if tenant_id.lower() in RESERVED_TENANT_IDS: + if tenant_id in RESERVED_TENANT_IDS: return None with get_db() as conn: @@ -366,10 +400,12 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N # Create new tenant with default credentials default_username = username or 'admin' default_password = password or 'admin' + # Preserve original casing for display name + display_name = tenant_id.replace('-', ' ').replace('_', ' ').title() cursor.execute(''' INSERT INTO tenants (id, name, username, password) VALUES (?, ?, ?, ?) - ''', (tenant_id, tenant_id.title(), default_username, default_password)) + ''', (tenant_id, display_name, default_username, default_password)) conn.commit() # Initialize default AR fields for the new tenant @@ -377,7 +413,7 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N return { 'id': tenant_id, - 'name': tenant_id.title(), + 'name': display_name, 'username': default_username, 'password': default_password } @@ -554,4 +590,30 @@ def get_product_image_by_field(product_id: str, tenant_id: str, field_name: str) row = cursor.fetchone() if row and row['image_data']: return row['image_data'], row['image_mime_type'] - return None \ No newline at end of file + return None + +def get_setting(key: str, default_value: str = None) -> Optional[str]: + """Get a setting value by key""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT value FROM settings WHERE key = ?', (key,)) + row = cursor.fetchone() + + if row: + return row['value'] + return default_value + +def set_setting(key: str, value: str): + """Set a setting value""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO settings (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP + ''', (key, value, value)) + conn.commit() + +def get_server_url() -> str: + """Get the configured server URL or return a default""" + return get_setting('server_url', 'http://localhost:5000') \ No newline at end of file diff --git a/src/templates/settings.html b/src/templates/settings.html new file mode 100644 index 0000000..0cc6697 --- /dev/null +++ b/src/templates/settings.html @@ -0,0 +1,52 @@ + + + + + + Server Settings - KCAP Demo Server + + + +
+

Server Settings

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+
+

Server Configuration

+
+
+
+
+ + + + This URL will be used for generating Knox Capture AR Template URLs. + Include the protocol (http:// or https://) and port if needed. + +
+ + Back to Home +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/templates/tenant_selection.html b/src/templates/tenant_selection.html index 427672a..4dd29ed 100644 --- a/src/templates/tenant_selection.html +++ b/src/templates/tenant_selection.html @@ -38,6 +38,7 @@ {{ tenant.created_at }}

Username: {{ tenant.username }}

+

Knox Capture AR Template URL: {{ server_url }}/{{ tenant.id }}/

ID: {{ tenant.id }}
@@ -73,6 +74,7 @@ Note: When you access a tenant URL directly (e.g., /my-tenant/), it will be automatically created with default credentials (admin/admin).

+ Server Settings From 9f33e1aeb68dc4556eda1575158f65b566209f6c Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Thu, 19 Jun 2025 16:08:18 -0400 Subject: [PATCH 03/11] case insensitivity --- src/app.py | 9 +++++ src/database.py | 59 ++++++++++++++++++++++++++++- src/templates/tenant_selection.html | 7 +++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index f94377d..af741a4 100644 --- a/src/app.py +++ b/src/app.py @@ -8,6 +8,7 @@ from barcode.writer import ImageWriter from io import BytesIO import shutil from werkzeug.routing import BaseConverter +from functools import wraps app = Flask(__name__) # Set DATA_FOLDER to the absolute path of the data directory inside src @@ -17,6 +18,14 @@ app.config['SECRET_KEY'] = 'dev-key-for-demo-only' # Custom converter for tenant IDs class TenantConverter(BaseConverter): regex = '[a-zA-Z0-9_-]+' + + def to_python(self, value): + # Convert to lowercase when parsing from URL + return value.lower() + + def to_url(self, value): + # Convert to lowercase when generating URLs + return value.lower() app.url_map.converters['tenant'] = TenantConverter diff --git a/src/database.py b/src/database.py index 836f9a6..01c71d4 100644 --- a/src/database.py +++ b/src/database.py @@ -204,6 +204,10 @@ def migrate_from_json(): def get_all_products(tenant_id: str = None) -> Dict[str, List[Dict[str, Any]]]: """Get all products for a tenant in the legacy format""" + # Normalize tenant_id to lowercase if provided + if tenant_id: + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -248,6 +252,9 @@ def get_all_products(tenant_id: str = None) -> Dict[str, List[Dict[str, Any]]]: def get_product(product_id: str, tenant_id: str) -> Optional[List[Dict[str, Any]]]: """Get a single product by ID and tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -272,6 +279,9 @@ def get_product(product_id: str, tenant_id: str) -> Optional[List[Dict[str, Any] def save_product(product_id: str, tenant_id: str, fields: List[Dict[str, Any]], image_data: Optional[bytes] = None, image_mime_type: Optional[str] = None): """Save or update a product for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -336,6 +346,9 @@ def save_product(product_id: str, tenant_id: str, fields: List[Dict[str, Any]], def delete_product(product_id: str, tenant_id: str): """Delete a product for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute('DELETE FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) @@ -343,6 +356,9 @@ def delete_product(product_id: str, tenant_id: str): def get_product_image(product_id: str, tenant_id: str) -> Optional[tuple[bytes, str]]: """Get product image data and mime type for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute('SELECT image_data, image_mime_type FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) @@ -420,6 +436,9 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N def update_tenant_credentials(tenant_id: str, username: str, password: str): """Update tenant credentials""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -448,6 +467,9 @@ def get_all_tenants() -> List[Dict[str, Any]]: def delete_tenant(tenant_id: str): """Delete a tenant and all associated data""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() # Due to ON DELETE CASCADE, this will also delete all products and product_fields @@ -475,6 +497,9 @@ def cleanup_reserved_tenants(): def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]: """Get custom AR fields for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -499,6 +524,9 @@ def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]: def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int: """Save or update a custom AR field""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -539,6 +567,9 @@ def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int: def delete_custom_ar_field(tenant_id: str, field_id: int): """Delete a custom AR field""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -549,17 +580,38 @@ def delete_custom_ar_field(tenant_id: str, field_id: int): def init_default_ar_fields(tenant_id: str): """Initialize default AR fields for a new tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + default_fields = [ {"fieldName": "_id", "label": "Item ID", "fieldType": "TEXT", "editable": "false", "displayOrder": 1}, {"fieldName": "_price", "label": "Sale Price", "fieldType": "TEXT", "editable": "true", "displayOrder": 2}, {"fieldName": "_image", "label": "Image", "fieldType": "IMAGE_URI", "editable": "false", "displayOrder": 3} ] - for field in default_fields: - save_custom_ar_field(tenant_id, field) + with get_db() as conn: + cursor = conn.cursor() + for field in default_fields: + # Use INSERT OR IGNORE to avoid duplicate key errors + cursor.execute(''' + INSERT OR IGNORE INTO custom_ar_fields + (tenant_id, field_name, label, field_type, editable, display_order) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + tenant_id, + field['fieldName'], + field['label'], + field['fieldType'], + field.get('editable', 'true'), + field.get('displayOrder', 0) + )) + conn.commit() def save_product_image(product_id: str, tenant_id: str, field_name: str, image_data: bytes, mime_type: str): """Save an image for a specific field of a product""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -579,6 +631,9 @@ def save_product_image(product_id: str, tenant_id: str, field_name: str, image_d def get_product_image_by_field(product_id: str, tenant_id: str, field_name: str) -> Optional[tuple[bytes, str]]: """Get image data for a specific field of a product""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' diff --git a/src/templates/tenant_selection.html b/src/templates/tenant_selection.html index 4dd29ed..5e2012f 100644 --- a/src/templates/tenant_selection.html +++ b/src/templates/tenant_selection.html @@ -90,11 +90,14 @@ e.preventDefault(); const tenantId = document.getElementById('tenantId').value; if (tenantId) { - if (reservedIds.includes(tenantId.toLowerCase())) { + // Convert to lowercase for consistency + const normalizedId = tenantId.toLowerCase(); + if (reservedIds.includes(normalizedId)) { alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`); return; } - window.location.href = '/' + tenantId + '/'; + // Use the normalized (lowercase) ID in the URL + window.location.href = '/' + normalizedId + '/'; } }); From 6dc276a2925d45fa0bb7be8201605632e9af8115 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Wed, 25 Jun 2025 18:11:12 -0400 Subject: [PATCH 04/11] fixing missing barcodes --- src/app.py | 49 +++++++++- src/templates/admin/generate_barcode.html | 4 +- src/templates/index.html | 89 ++++++++++++------ src/templates/tenant_settings.html | 104 ++++++++++++++++++++++ 4 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 src/templates/tenant_settings.html diff --git a/src/app.py b/src/app.py index af741a4..2842682 100644 --- a/src/app.py +++ b/src/app.py @@ -70,7 +70,46 @@ def tenant_index(tenant_id): if tenant is None: # Reserved tenant ID or invalid return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404 - return render_template('index.html', tenant=tenant) + + # Load products for this tenant + products = load_products(tenant_id) + + return render_template('index.html', tenant=tenant, products=products) + +@app.route('//settings') +def tenant_settings(tenant_id): + # Get tenant + tenant = database.get_or_create_tenant(tenant_id) + if tenant is None: + return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404 + + # Get custom AR fields for this tenant + custom_fields = database.get_custom_ar_fields(tenant_id) + + # Get server URL for API endpoint display + server_url = database.get_server_url() + + return render_template('tenant_settings.html', tenant=tenant, custom_fields=custom_fields, server_url=server_url) + +@app.route('//settings/credentials', methods=['POST']) +def update_tenant_credentials(tenant_id): + # Get tenant + tenant = database.get_tenant(tenant_id) + if tenant is None: + return jsonify({"error": "Tenant not found"}), 404 + + username = request.form.get('username', '').strip() + password = request.form.get('password', '').strip() + + if not username: + flash('Username is required.', 'error') + return redirect(f'/{tenant_id}/settings') + + # Update credentials + database.update_tenant_credentials(tenant_id, username, password if password else None) + + flash('Credentials updated successfully.', 'success') + return redirect(f'/{tenant_id}/settings') def check_basic_auth(auth_header, tenant_id): """Validate Basic authentication credentials for a tenant""" @@ -119,10 +158,14 @@ def get_ar_info(tenant_id): def filter_and_process_fields(product_fields): # Filter to only include fields defined in custom AR fields filtered_fields = [] + + # Get field types for all custom fields + field_types = {f['fieldName']: f['fieldType'] for f in custom_fields} + for field in product_fields: if field['fieldName'] in custom_field_names: - # Create absolute URL for image fields - if field['fieldName'] == '_image' and field['value']: + # Create absolute URL for IMAGE_URI fields + if field_types.get(field['fieldName']) == 'IMAGE_URI' and field['value']: # If it's already an absolute URL, leave it as is if not field['value'].startswith('http'): # Build absolute URL using request host with tenant diff --git a/src/templates/admin/generate_barcode.html b/src/templates/admin/generate_barcode.html index 0615bf5..19a0d8c 100644 --- a/src/templates/admin/generate_barcode.html +++ b/src/templates/admin/generate_barcode.html @@ -86,8 +86,8 @@ diff --git a/src/templates/index.html b/src/templates/index.html index b20730a..43ccb8f 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -5,37 +5,76 @@ {% block content %}
-
+
+

KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}

Manage your product catalog and AR content for barcode scanning applications.


-

Quick Actions

-
-
-
-
-
Product Management
-

Add, edit, and manage your product catalog with custom fields.

- {% if tenant %} - Manage Products - {% endif %} -
-
-
-
-
-
-
AR Field Configuration
-

Customize the fields returned by AR content endpoints.

- {% if tenant %} - Configure AR Fields - {% endif %} -
-
-
+ + + {% if products %} +
+ + + + + + + + + + + {% for product_id, product_data in products.items() %} + + + + + + + {% endfor %} + +
Product IDNamePriceActions
{{ product_id }} + {% for field in product_data %} + {% if field.fieldName == '_name' %} + {{ field.value }} + {% endif %} + {% endfor %} + + {% for field in product_data %} + {% if field.fieldName == '_price' %} + {{ field.value }} + {% endif %} + {% endfor %} + + + Edit + + + Barcodes + +
+
+ + {% else %} +
+ No products available yet. Click "Add New Product" to get started. +
+ {% endif %}
diff --git a/src/templates/tenant_settings.html b/src/templates/tenant_settings.html new file mode 100644 index 0000000..d6270ef --- /dev/null +++ b/src/templates/tenant_settings.html @@ -0,0 +1,104 @@ +{% extends "layout.html" %} + +{% block title %}Settings - {{ tenant.name }} - KCAP Demo Server{% endblock %} + +{% block content %} +
+
+
+

Settings - {{ tenant.name }}

+ + Back to Dashboard + +
+ +
+ +
+
+
+
Device Authentication
+
+
+

Configure the username and password for device authentication.

+ +
+ + +
+
+ + +
Leave blank to keep current password
+
+ + +
+
+
+ + +
+
+
+
Custom AR Content Fields
+
+
+

Manage the fields that are returned in the AR content API.

+ + Manage AR Fields + + + {% if custom_fields %} +
+
Current Fields:
+
    + {% for field in custom_fields %} +
  • +
    + {{ field.label }} + ({{ field.fieldName }}) +
    + {{ field.fieldType }} +
  • + {% endfor %} +
+
+ {% endif %} +
+
+
+
+ + +
+
+
+
+
Tenant Information
+
+
+
+
Tenant ID:
+
{{ tenant.id }}
+ +
Tenant Name:
+
{{ tenant.name }}
+ +
API Endpoint:
+
{{ server_url }}/{{ tenant.id }}/arinfo
+ +
Created:
+
{{ tenant.created_at }}
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file From 92eb45fc8ca2cf00638453b14306ba6b7da820f1 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 16:45:58 -0400 Subject: [PATCH 05/11] Fix product image not saving/displaying in tenant admin Fixed two issues with product images: 1. Images weren't being preserved when editing other product fields - added logic to keep existing image value when no new image is uploaded 2. Image URLs had double underscores (e.g., /images/123__image.jpg) due to field names starting with underscore - now strips leading underscore from field name when constructing image paths --- src/routes/admin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/routes/admin.py b/src/routes/admin.py index 7bb087e..82c692c 100644 --- a/src/routes/admin.py +++ b/src/routes/admin.py @@ -110,8 +110,9 @@ def add_product(tenant_id): 'mime_type': mime_type }) - # Set the image URL path - image_path = f"/images/{product_id}_{field_name}.{extension}" + # Set the image URL path (strip leading underscore from field_name to avoid double underscores) + field_suffix = field_name[1:] if field_name.startswith('_') else field_name + image_path = f"/images/{product_id}_{field_suffix}.{extension}" # For backward compatibility with _image field if field_name == '_image': @@ -231,7 +232,7 @@ def edit_product(tenant_id, product_id): # Read image data file.seek(0) img_data = file.read() - + # Determine mime type extension = file.filename.rsplit('.', 1)[1].lower() mime_types = { @@ -241,21 +242,23 @@ def edit_product(tenant_id, product_id): 'gif': 'image/gif' } mime_type = mime_types.get(extension, 'image/jpeg') - + # Store for later saving images_to_save.append({ 'field_name': field_name, 'data': img_data, 'mime_type': mime_type }) - - # Update image path - existing_field["value"] = f"/images/{product_id}_{field_name}.{extension}" - + + # Update image path (strip leading underscore from field_name to avoid double underscores) + field_suffix = field_name[1:] if field_name.startswith('_') else field_name + existing_field["value"] = f"/images/{product_id}_{field_suffix}.{extension}" + # For backward compatibility with _image field if field_name == '_image': image_data = img_data image_mime_type = mime_type + # If no new image uploaded, keep the existing value (already in existing_field) else: # Get value from form value = request.form.get(f'field_{field_name}', '') From f781cdf602c6ee8e593ee9f779c6ace20d5e752f Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 17:11:26 -0400 Subject: [PATCH 06/11] Add dynamic custom field display and remove redundant admin routes Major improvements to tenant management: 1. Dynamic field display - Product lists now automatically show all custom fields based on tenant configuration (e.g., inventory, custom attributes) 2. Fixed Jinja2 template scoping issue - Product field values now correctly display in edit forms using selectattr filter 3. Simplified URL structure - Removed redundant /tenant/admin/ routes, all product management now at /tenant/ root 4. Enhanced tenant landing page - Added delete functionality with confirmation modal, shows all custom fields except images 5. Updated all routes and redirects to use simplified paths (/tenant/add, /tenant/edit, etc.) This consolidates the interface so /tenant/ serves as the main dashboard with full product management capabilities, while /tenant/settings handles configuration. --- src/app.py | 30 +++---- src/routes/admin.py | 37 ++++----- src/templates/admin/add_product.html | 4 +- src/templates/admin/edit_product.html | 17 ++-- src/templates/admin/generate_barcode.html | 2 +- src/templates/admin/index.html | 58 +++++++------- src/templates/index.html | 97 +++++++++++++++++------ 7 files changed, 142 insertions(+), 103 deletions(-) diff --git a/src/app.py b/src/app.py index 2842682..456a73a 100644 --- a/src/app.py +++ b/src/app.py @@ -70,11 +70,12 @@ def tenant_index(tenant_id): if tenant is None: # Reserved tenant ID or invalid return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404 - + # Load products for this tenant products = load_products(tenant_id) - - return render_template('index.html', tenant=tenant, products=products) + custom_fields = database.get_custom_ar_fields(tenant_id) + + return render_template('index.html', tenant=tenant, products=products, custom_fields=custom_fields) @app.route('//settings') def tenant_settings(tenant_id): @@ -292,21 +293,16 @@ def serve_barcode(tenant_id, filename): app.logger.error(f"Barcode generation error: {str(e)}") return jsonify({"error": "Failed to generate barcode"}), 500 -# Import the admin routes directly and register them with tenant support -from routes.admin import (index as admin_index, add_product, edit_product, - delete_product, view_product, generate_barcode, - generate_barcode_page, manage_credentials, manage_ar_fields) +# Import product management routes +from routes.admin import (add_product, edit_product, delete_product, + generate_barcode, generate_barcode_page) -# Register admin routes with tenant prefix -app.add_url_rule('//admin/', 'admin.index', admin_index) -app.add_url_rule('//admin/add', 'admin.add_product', add_product, methods=['GET', 'POST']) -app.add_url_rule('//admin/edit/', 'admin.edit_product', edit_product, methods=['GET', 'POST']) -app.add_url_rule('//admin/delete/', 'admin.delete_product', delete_product, methods=['POST']) -app.add_url_rule('//admin/view/', 'admin.view_product', view_product) -app.add_url_rule('//admin/generate_barcode//', 'admin.generate_barcode', generate_barcode) -app.add_url_rule('//admin/generate_barcode_page/', 'admin.generate_barcode_page', generate_barcode_page) -app.add_url_rule('//admin/credentials', 'admin.manage_credentials', manage_credentials, methods=['GET', 'POST']) -app.add_url_rule('//admin/ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST']) +# Register product management routes (remove /admin/ from paths) +app.add_url_rule('//add', 'admin.add_product', add_product, methods=['GET', 'POST']) +app.add_url_rule('//edit/', 'admin.edit_product', edit_product, methods=['GET', 'POST']) +app.add_url_rule('//delete/', 'admin.delete_product', delete_product, methods=['POST']) +app.add_url_rule('//generate_barcode//', 'admin.generate_barcode', generate_barcode) +app.add_url_rule('//generate_barcode_page/', 'admin.generate_barcode_page', generate_barcode_page) # Register API routes with tenant prefix from routes.api import api_index diff --git a/src/routes/admin.py b/src/routes/admin.py index 82c692c..1b24498 100644 --- a/src/routes/admin.py +++ b/src/routes/admin.py @@ -38,7 +38,8 @@ def save_products(products): def index(tenant_id): products = load_products(tenant_id) tenant = database.get_or_create_tenant(tenant_id) - return render_template('admin/index.html', products=products, tenant=tenant) + custom_fields = database.get_custom_ar_fields(tenant_id) + return render_template('admin/index.html', products=products, tenant=tenant, custom_fields=custom_fields) def add_product(tenant_id): # Get custom AR fields for this tenant @@ -172,26 +173,26 @@ def add_product(tenant_id): database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) flash('Product added successfully!') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields) def edit_product(tenant_id, product_id): products = load_products(tenant_id) custom_fields = database.get_custom_ar_fields(tenant_id) - + if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') if request.method == 'POST': # Handle backward compatibility image upload image_data = None image_mime_type = None - + # Process images after product is saved images_to_save = [] - + # Update fields based on form data updated_product = [] @@ -295,7 +296,7 @@ def edit_product(tenant_id, product_id): database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) flash('Product updated successfully!') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') return render_template('admin/edit_product.html', product_id=product_id, @@ -305,24 +306,24 @@ def edit_product(tenant_id, product_id): def delete_product(tenant_id, product_id): products = load_products(tenant_id) - + if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index', tenant_id=tenant_id)) - + return redirect(f'/{tenant_id}/') + # Delete product from database (image is stored in DB) database.delete_product(product_id, tenant_id) - + flash('Product deleted successfully!') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') def view_product(tenant_id, product_id): products = load_products(tenant_id) - + if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index', tenant_id=tenant_id)) - + return redirect(f'/{tenant_id}/') + return render_template('admin/view_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) def generate_barcode(tenant_id, product_id, code_type): @@ -346,10 +347,10 @@ def generate_barcode(tenant_id, product_id, code_type): def generate_barcode_page(tenant_id, product_id): """Show a page with different barcode options for a product""" products = load_products(tenant_id) - + if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') # Extract product data product_data = {} @@ -388,7 +389,7 @@ def manage_credentials(tenant_id): if username and password: database.update_tenant_credentials(tenant_id, username, password) flash('Credentials updated successfully!') - return redirect(url_for('admin.index', tenant_id=tenant_id)) + return redirect(f'/{tenant_id}/') else: flash('Username and password are required.') diff --git a/src/templates/admin/add_product.html b/src/templates/admin/add_product.html index 099495a..d38843b 100644 --- a/src/templates/admin/add_product.html +++ b/src/templates/admin/add_product.html @@ -10,7 +10,7 @@

Add New Product

-
+
@@ -67,7 +67,7 @@ {% endif %}
- Cancel + Cancel
diff --git a/src/templates/admin/edit_product.html b/src/templates/admin/edit_product.html index e7f85ad..16873bb 100644 --- a/src/templates/admin/edit_product.html +++ b/src/templates/admin/edit_product.html @@ -10,7 +10,7 @@

Edit Product

-
+
@@ -21,14 +21,11 @@ {% if field.fieldName != '_id' %}
- - {% set current_value = '' %} - {% for prod_field in product %} - {% if prod_field.fieldName == field.fieldName %} - {% set current_value = prod_field.value %} - {% endif %} - {% endfor %} - + + {# Find current value using selectattr filter #} + {% set matching_fields = product|selectattr('fieldName', 'equalto', field.fieldName)|list %} + {% set current_value = matching_fields[0].value if matching_fields else '' %} + {% if field.fieldType == 'IMAGE_URI' %}
Leave empty to keep the current image.
@@ -93,7 +90,7 @@ {% endif %}
- Cancel + Cancel
diff --git a/src/templates/admin/generate_barcode.html b/src/templates/admin/generate_barcode.html index 19a0d8c..dd94df4 100644 --- a/src/templates/admin/generate_barcode.html +++ b/src/templates/admin/generate_barcode.html @@ -8,7 +8,7 @@

Generate Barcodes for Product: {{ product_id }}

- Back to Products + Back to Products
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %} diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index 4371b28..d005c1c 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -22,46 +22,44 @@ - - - - - + + {% for custom_field in custom_fields %} + {% if custom_field.fieldName != '_id' %} + + {% endif %} + {% endfor %} + {% for product_id, product_data in products.items() %} - - - + {% for custom_field in custom_fields %} + {% if custom_field.fieldName != '_id' %} + + {% endif %} + {% endfor %} - - + {% for custom_field in custom_fields %} + {% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %} + + {% endif %} + {% endfor %} @@ -38,45 +41,42 @@ {% for product_id, product_data in products.items() %} + {% for custom_field in custom_fields %} + {% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %} + + {% endif %} + {% endfor %} - - {% endfor %}
Product IDNamePriceImageActionsProduct ID{{ custom_field.label }}Actions
{{ product_id }} - {% for field in product_data %} - {% if field.fieldName == '_name' %} - {{ field.value }} - {% endif %} - {% endfor %} - - {% for field in product_data %} - {% if field.fieldName == '_price' %} - {{ field.value }} - {% endif %} - {% endfor %} - - {% for field in product_data %} - {% if field.fieldName == '_image' %} -
- Product image -
- {% endif %} - {% endfor %} -
+ {% for field in product_data %} + {% if field.fieldName == custom_field.fieldName %} + {% if custom_field.fieldType == 'IMAGE_URI' %} + {% if field.value %} +
+ {{ custom_field.label }} +
+ {% endif %} + {% else %} + {{ field.value }} + {% endif %} + {% endif %} + {% endfor %} +
Edit Barcodes -
Product IDNamePrice{{ custom_field.label }}Actions
{{ product_id }} + {% set matching_fields = product_data|selectattr('fieldName', 'equalto', custom_field.fieldName)|list %} + {{ matching_fields[0].value if matching_fields else '' }} + - {% for field in product_data %} - {% if field.fieldName == '_name' %} - {{ field.value }} - {% endif %} - {% endfor %} - - {% for field in product_data %} - {% if field.fieldName == '_price' %} - {{ field.value }} - {% endif %} - {% endfor %} - - + Edit - + Barcodes + +
- {% else %}
No products available yet. Click "Add New Product" to get started.
{% endif %}
- +

Getting Started

    @@ -89,4 +89,51 @@
+ + + +{% endblock %} + +{% block extra_js %} + {% endblock %} From ccac3134f3579a58868c974a5878e8de5d73fdf1 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 17:17:58 -0400 Subject: [PATCH 07/11] Fix image display and add AR fields management route Fixes: 1. Image display - Images now properly display on tenant landing page and edit forms by prepending tenant ID to image paths (/tenant/images/... instead of /images/...) 2. AR fields route - Added /tenant/ar_fields route (without /admin/ prefix) for managing custom AR content fields 3. Template updates - Updated all image src attributes to include tenant prefix for proper routing 4. Navigation - Updated AR fields back button to return to settings page instead of non-existent admin index Images are now visible in: - Product list on tenant landing page - Product edit forms showing current images - Admin index (if accessed) --- src/app.py | 3 ++- src/templates/admin/ar_fields.html | 2 +- src/templates/admin/edit_product.html | 2 +- src/templates/admin/index.html | 2 +- src/templates/index.html | 14 +++++++++++--- src/templates/tenant_settings.html | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app.py b/src/app.py index 456a73a..9ba7d84 100644 --- a/src/app.py +++ b/src/app.py @@ -295,7 +295,7 @@ def serve_barcode(tenant_id, filename): # Import product management routes from routes.admin import (add_product, edit_product, delete_product, - generate_barcode, generate_barcode_page) + generate_barcode, generate_barcode_page, manage_ar_fields) # Register product management routes (remove /admin/ from paths) app.add_url_rule('//add', 'admin.add_product', add_product, methods=['GET', 'POST']) @@ -303,6 +303,7 @@ app.add_url_rule('//edit/', 'admin.edit_product', app.add_url_rule('//delete/', 'admin.delete_product', delete_product, methods=['POST']) app.add_url_rule('//generate_barcode//', 'admin.generate_barcode', generate_barcode) app.add_url_rule('//generate_barcode_page/', 'admin.generate_barcode_page', generate_barcode_page) +app.add_url_rule('//ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST']) # Register API routes with tenant prefix from routes.api import api_index diff --git a/src/templates/admin/ar_fields.html b/src/templates/admin/ar_fields.html index b49f93b..2b26339 100644 --- a/src/templates/admin/ar_fields.html +++ b/src/templates/admin/ar_fields.html @@ -8,7 +8,7 @@

Manage AR Content Fields

Configure the fields that will be returned by the /arcontentfields and /arinfo endpoints.

- Back to Admin + Back to Settings
diff --git a/src/templates/admin/edit_product.html b/src/templates/admin/edit_product.html index 16873bb..a2f3680 100644 --- a/src/templates/admin/edit_product.html +++ b/src/templates/admin/edit_product.html @@ -34,7 +34,7 @@
- Current image + Current image
{% endif %} diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index d005c1c..fb3fc22 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -43,7 +43,7 @@ {% if custom_field.fieldType == 'IMAGE_URI' %} {% if field.value %}
- {{ custom_field.label }} + {{ custom_field.label }}
{% endif %} {% else %} diff --git a/src/templates/index.html b/src/templates/index.html index b03129a..7f394b3 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -30,7 +30,7 @@ Product ID {% for custom_field in custom_fields %} - {% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %} + {% if custom_field.fieldName != '_id' %} {{ custom_field.label }} {% endif %} {% endfor %} @@ -42,10 +42,18 @@ {{ product_id }} {% for custom_field in custom_fields %} - {% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %} + {% if custom_field.fieldName != '_id' %} {% set matching_fields = product_data|selectattr('fieldName', 'equalto', custom_field.fieldName)|list %} - {{ matching_fields[0].value if matching_fields else '' }} + {% if custom_field.fieldType == 'IMAGE_URI' %} + {% if matching_fields and matching_fields[0].value %} +
+ {{ custom_field.label }} +
+ {% endif %} + {% else %} + {{ matching_fields[0].value if matching_fields else '' }} + {% endif %} {% endif %} {% endfor %} diff --git a/src/templates/tenant_settings.html b/src/templates/tenant_settings.html index d6270ef..c39a0f8 100644 --- a/src/templates/tenant_settings.html +++ b/src/templates/tenant_settings.html @@ -49,7 +49,7 @@

Manage the fields that are returned in the AR content API.

- + Manage AR Fields From 253058b45b619289967608f0538feffb23b5075c Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 17:40:04 -0400 Subject: [PATCH 08/11] Add QR code functionality for AR template URLs - Add QR code icon next to each tenant's AR template URL on main page - Add modal popup to display dynamically generated QR codes - Add routes for QR code generation (/qrcode/template and /qrcode/arinfo) - Include Bootstrap Icons for QR code icon display --- src/app.py | 100 ++++++++++++++++++++++++++-- src/templates/tenant_selection.html | 35 +++++++++- src/templates/tenant_settings.html | 29 +++++++- 3 files changed, 154 insertions(+), 10 deletions(-) diff --git a/src/app.py b/src/app.py index 9ba7d84..7228860 100644 --- a/src/app.py +++ b/src/app.py @@ -146,23 +146,64 @@ def get_ar_content_fields(tenant_id): fields = database.get_custom_ar_fields(tenant_id) return jsonify(fields), 200 -@app.route('//arinfo', methods=['GET']) +@app.route('//arinfo', methods=['GET', 'POST']) def get_ar_info(tenant_id): barcode = request.args.get('barcode') products = load_products(tenant_id) - + # Get custom AR fields for this tenant custom_fields = database.get_custom_ar_fields(tenant_id) custom_field_names = [f['fieldName'] for f in custom_fields] - + + # Handle POST request - update product fields + if request.method == 'POST': + if not barcode: + return jsonify({"error": "Barcode parameter required"}), 400 + + if barcode not in products: + return jsonify({"error": "Product not found"}), 404 + + try: + # Get the updated fields from the request body + updated_fields = request.get_json() + + if not isinstance(updated_fields, list): + return jsonify({"error": "Request body must be an array of fields"}), 400 + + # Get current product data + current_product = products[barcode] + + # Update only the editable fields + for updated_field in updated_fields: + field_name = updated_field.get('fieldName') + new_value = updated_field.get('value') + + # Find the field in the current product and update it + for field in current_product: + if field['fieldName'] == field_name: + # Check if the field is editable + if field.get('editable') == 'true': + field['value'] = new_value + break + + # Save the updated product to database + database.save_product(barcode, tenant_id, current_product) + + app.logger.info(f"Updated product {barcode} for tenant {tenant_id}") + return jsonify({"success": True}), 200 + + except Exception as e: + app.logger.error(f"Error updating product: {str(e)}") + return jsonify({"error": "Failed to update product"}), 500 + # Helper function to convert relative image paths to absolute URLs and filter fields def filter_and_process_fields(product_fields): # Filter to only include fields defined in custom AR fields filtered_fields = [] - + # Get field types for all custom fields field_types = {f['fieldName']: f['fieldType'] for f in custom_fields} - + for field in product_fields: if field['fieldName'] in custom_field_names: # Create absolute URL for IMAGE_URI fields @@ -173,7 +214,8 @@ def get_ar_info(tenant_id): field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" filtered_fields.append(field) return filtered_fields - + + # Handle GET request - return product data # If barcode is provided, return specific product if barcode: if barcode in products: @@ -182,7 +224,7 @@ def get_ar_info(tenant_id): response.headers['Access-Control-Allow-Origin'] = '*' return response, 200 return jsonify({"error": "Product not found"}), 404 - + # Return all products if no barcode specified # Convert all products to have absolute URLs and filter fields all_products = {} @@ -241,6 +283,50 @@ def serve_image(tenant_id, filename): app.logger.warning(f"Image not found: {filename}") return jsonify({"error": "Image not found"}), 404 +@app.route('//qrcode/template', methods=['GET']) +def generate_template_qr_code(tenant_id): + """Generate QR code for the AR Template URL""" + try: + # Get server URL from settings + server_url = database.get_server_url() + template_url = f"{server_url}/{tenant_id}/" + + # Generate QR code + buffer = BytesIO() + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(template_url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save(buffer, format='PNG') + buffer.seek(0) + + return Response(buffer.getvalue(), mimetype='image/png') + except Exception as e: + app.logger.error(f"QR code generation error: {str(e)}") + return jsonify({"error": "Failed to generate QR code"}), 500 + +@app.route('//qrcode/arinfo', methods=['GET']) +def generate_ar_qr_code(tenant_id): + """Generate QR code for the AR API endpoint""" + try: + # Get server URL from settings + server_url = database.get_server_url() + ar_url = f"{server_url}/{tenant_id}/arinfo" + + # Generate QR code + buffer = BytesIO() + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(ar_url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save(buffer, format='PNG') + buffer.seek(0) + + return Response(buffer.getvalue(), mimetype='image/png') + except Exception as e: + app.logger.error(f"QR code generation error: {str(e)}") + return jsonify({"error": "Failed to generate QR code"}), 500 + @app.route('//barcodes/', methods=['GET']) def serve_barcode(tenant_id, filename): # Parse filename to extract product_id and barcode type diff --git a/src/templates/tenant_selection.html b/src/templates/tenant_selection.html index 5e2012f..14ee745 100644 --- a/src/templates/tenant_selection.html +++ b/src/templates/tenant_selection.html @@ -5,6 +5,7 @@ Select or Create Tenant - KCAP Demo Server +
@@ -38,9 +39,15 @@ {{ tenant.created_at }}

Username: {{ tenant.username }}

-

Knox Capture AR Template URL: {{ server_url }}/{{ tenant.id }}/

+

+ Knox Capture AR Template URL: + {{ server_url }}/{{ tenant.id }}/ +

ID: {{ tenant.id }} +
@@ -79,7 +86,31 @@
- + + + {% for tenant in tenants %} + + {% endfor %} + +{% endblock %} diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index fb3fc22..bfac565 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -10,6 +10,9 @@
diff --git a/src/templates/index.html b/src/templates/index.html index 65e1a0f..714a997 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -18,9 +18,14 @@