This commit is contained in:
Jason
2026-04-11 00:04:09 -05:00
commit 7a2fffd62e
110 changed files with 51809 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Flask Configuration
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY="3pgys!#g$wA7V#%sS6GiDKEA!gwHjGR2"
# Server Configuration
HOST=0.0.0.0
PORT=8080
# Database (if needed in future)
# DATABASE_URL=sqlite:///products.db
# source /home/bmdwtjuw/virtualenv/product-finder/3.6/bin/activate && cd /home/bmdwtjuw/product-finder
+61
View File
@@ -0,0 +1,61 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Flask
instance/
.webassets-cache
# Environment
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Logs
*.log
logs/
# Database
*.db
*.sqlite
*.sqlite3
# Testing
.pytest_cache/
.coverage
htmlcov/
# Production
*.pid
*.sock
+93
View File
@@ -0,0 +1,93 @@
# BEGIN Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
# Skip ACME challenge
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
# Redirect HTTP to HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# END Force HTTPS
# BEGIN LSCACHE
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule litespeed/debug/.*\.log$ - [F,L]
RewriteRule \.litespeed_conf\.dat - [F,L]
</IfModule>
<IfModule LiteSpeed>
CacheLookup on
RewriteRule .* - [E=Cache-Control:no-autoflush]
### marker ASYNC start ###
RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php
RewriteCond %{QUERY_STRING} action=async_litespeed
RewriteRule .* - [E=noabort:1]
### marker ASYNC end ###
### marker DROPQS start ###
CacheKeyModify -qs:fbclid
CacheKeyModify -qs:gclid
CacheKeyModify -qs:utm*
CacheKeyModify -qs:_ga
### marker DROPQS end ###
</IfModule>
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
# END LSCACHE
# BEGIN NON_LSCACHE
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ##
# END NON_LSCACHE
# BEGIN Security Block
# Block the include-only files.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^wp-admin/includes/ - [F,L]
RewriteRule !^wp-includes/ - [S=3]
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
RewriteRule ^wp-includes/theme-compat/ - [F,L]
</IfModule>
# Disable directory listing
Options All -Indexes
# Remove header with PHP version
Header always unset X-Powered-By
Header unset X-Powered-By
# END Security Block
# BEGIN LiteSpeed
# The directives (lines) between "BEGIN LiteSpeed" and "END LiteSpeed" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule Litespeed>
SetEnv noabort 1
</IfModule>
# END LiteSpeed
# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
# *** IMPORTANT: Exclude product-finder from WordPress routing ***
RewriteCond %{REQUEST_URI} !^/product-finder/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
+243
View File
@@ -0,0 +1,243 @@
# Dynamic Image Generation - Quick Start
## What Was Created
I've built a complete server-side image generation system that can dynamically create product images with different colors and configurations. Here's what's included:
### Files Created:
1. **`app/image_generator.py`** - Core image processing logic using PIL/Pillow
2. **`app/data/image_configs.json`** - Configuration for COBRAI product with colorable regions
3. **`app/templates/image_test.html`** - Test page to demonstrate the API
4. **`information/DYNAMIC_IMAGE_API.md`** - Complete documentation
### Files Modified:
1. **`app/app.py`** - Added 4 new API endpoints
2. **`app/requirements.txt`** - Added Pillow dependency
## ⚠️ Important: Image File
You need to manually save the storm door image as:
```
app/images/cobrai.jpg
```
I created a placeholder file, but you need to replace it with the actual image from your screenshot (the white storm door with two screen panels).
## Setup & Testing
### 1. Install Dependencies
```bash
cd app
pip install Pillow
```
### 2. Add the Image
Save the storm door image as `app/images/cobrai.jpg`
### 3. Start the Server
```bash
python app.py
```
### 4. Test the API
#### Option A: Visit Test Page
Open browser to:
```
http://localhost:8080/image-test
```
This interactive page lets you:
- Select colors (White, Black, Bronze, Sandstone)
- Choose hinge side (Left/Right)
- See both image rendering methods
- View the product configuration
- Clear the cache
#### Option B: Direct URL Test
Visit these URLs in your browser:
```
http://localhost:8080/api/product-image/cobrai?color=black&hinge=right
http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left
http://localhost:8080/api/product-config/cobrai
```
#### Option C: Using cURL
```bash
# Get black door image
curl http://localhost:8080/api/product-image/cobrai?color=black > test_black.png
# Get bronze left-hinge door
curl http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left > test_bronze_left.png
# Get configuration
curl http://localhost:8080/api/product-config/cobrai
# Get as JSON with base64
curl http://localhost:8080/api/product-image/cobrai?color=white&format=json
```
## How It Works
### The Configuration
The `image_configs.json` file defines:
1. **Colorable Regions** - Rectangular areas to recolor:
- Left frame: [0, 0] to [100, 1450]
- Right frame: [620, 0] to [720, 1450]
- Top rail: [0, 0] to [720, 80]
- Middle rail: [0, 590] to [720, 680]
- Bottom panel: [100, 1090] to [620, 1450]
2. **Available Colors**:
- White: RGB [255, 255, 255]
- Black: RGB [30, 30, 30]
- Bronze: RGB [110, 80, 50]
- Sandstone: RGB [210, 190, 165]
3. **Hardware Positions** - Where handles go (placeholder for future):
- Right hinge: position [580, 730]
- Left hinge: position [140, 730] (flipped)
### The Process
1. Load white storm door image
2. For each colorable region:
- Calculate pixel brightness
- Apply target color while preserving brightness
- This maintains shadows/highlights
3. Optionally flip for left hinge
4. Cache the result for fast subsequent requests
## API Endpoints
### 1. Generate Image
```
GET /api/product-image/<product_code>?color=<color>&hinge=<hinge>
```
**Parameters:**
- `color`: white, black, bronze, sandstone
- `hinge`: left, right
- `format`: image (default) or json
- `cache`: true (default) or false
**Returns:** PNG image or JSON with base64
### 2. Get Configuration
```
GET /api/product-config/<product_code>
```
**Returns:** JSON with all product config (colors, regions, etc.)
### 3. Clear Cache
```
POST /api/clear-image-cache
```
**Body (optional):**
```json
{"productCode": "cobrai"}
```
## Adjusting the Configuration
### If you need to change color regions:
1. Open the image in an image editor (Paint, Photoshop, etc.)
2. Note the pixel coordinates of the area you want to color
3. Edit `app/data/image_configs.json`:
```json
{
"name": "frame",
"topLeft": [x1, y1],
"bottomRight": [x2, y2]
}
```
### If you need to adjust colors:
Edit the RGB values in `availableColors`:
```json
"bronze": {
"rgb": [110, 80, 50], // Adjust these numbers
"name": "Bronze"
}
```
### If hardware images exist:
Place hardware PNGs in `app/images/hardware/` and update config:
```json
"hardwarePositions": {
"handle_right": {
"x": 580,
"y": 730,
"image": "images/hardware/handle-lever.png"
}
}
```
## Next Steps
### To integrate into your product finder:
Replace the layered image system with dynamic generation:
```javascript
// Instead of static layers
imageDisplayHtml = `<img src="/api/product-image/${prodCode}?color=${color}&hinge=${hinge}">`;
// Or update existing image on change
function updateProductPreview(productCode) {
const color = document.getElementById('config-color').value;
const hinge = document.querySelector('[name="hinge-location"]:checked').value;
document.getElementById('product-image').src =
`/api/product-image/${productCode}?color=${color}&hinge=${hinge}`;
}
```
### To add more products:
1. Take photo of product in white/neutral color
2. Save as `app/images/productcode.jpg`
3. Add configuration to `image_configs.json`
4. Define colorable regions using image editor coordinates
5. Test at `/image-test`
## Troubleshooting
**"PIL/Pillow not installed"**
```bash
pip install Pillow
```
**"Product cobrai not found"**
- Check that image exists at `app/images/cobrai.jpg`
- Check that config exists in `image_configs.json`
**Colors look weird**
- Adjust RGB values in config
- Ensure source image is white or neutral
- Check that region coordinates are correct
**Image not changing**
- Try clearing cache
- Set `cache=false` in URL parameter
- Check browser console for errors
## Performance
- **First request**: ~100-300ms (generates and caches)
- **Cached requests**: ~10-50ms (serves from cache)
- **Cache location**: `app/cache/product_images/`
## Full Documentation
See `information/DYNAMIC_IMAGE_API.md` for complete documentation including:
- Advanced configuration options
- Production deployment tips
- Frontend integration examples
- Adding glass tinting, textures, etc.
- Batch generation scripts
+51
View File
@@ -0,0 +1,51 @@
# Product Finder Application
A Flask-based web application for finding products through an interactive quiz interface.
## Quick Start
### 🚀 Running the Application
**Option 1: Using VS Code Tasks (Recommended)**
- Press `Ctrl+Shift+P` → Type "Tasks: Run Task" → Select "Start Flask Server"
- Open http://127.0.0.1:8080
**Option 2: Command Line**
```bash
cd app
python app.py
```
### 📊 Updating Product Data
When you update `app/data/products.csv`:
- Press `Ctrl+Shift+B` to process the data
- All JSON files will be regenerated automatically
## Project Structure
```
📁 app/ - All active project files (application code, assets, data)
📁 information/ - Documentation and deprecated files
```
See [information/FOLDER_STRUCTURE.md](information/FOLDER_STRUCTURE.md) for detailed organization.
## Documentation
- **[FOLDER_STRUCTURE.md](information/FOLDER_STRUCTURE.md)** - Complete workspace organization
- **[QUICKSTART.md](information/QUICKSTART.md)** - Getting started guide
- **[BITWISE_USAGE_GUIDE.md](information/BITWISE_USAGE_GUIDE.md)** - Bitwise filtering documentation
- **[START_HERE.md](information/START_HERE.md)** - Project overview
## Development
All development happens in the `app/` folder. The project uses:
- **Flask** for the web server
- **Python** for data processing
- **Vanilla JavaScript** for the frontend
---
**Version:** 2.0
**Last Updated:** March 26, 2026
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
# Apache configuration for Flask subdirectory deployment
# Passenger configuration ONLY - minimal to avoid blocking issues
PassengerEnabled on
PassengerAppRoot /home/bmdwtjuw/product-finder
PassengerPython /home/bmdwtjuw/virtualenv/product-finder/3.13/bin/python3.13_bin
SetEnv APPLICATION_ROOT /product-finder
+4
View File
@@ -0,0 +1,4 @@
# MINIMAL .htaccess for troubleshooting
# If you're getting 403/404, try this simplified version first
PassengerEnabled on
+98
View File
@@ -0,0 +1,98 @@
# Admin Utilities
This folder contains administrative tools and utilities for maintaining the CGW Product Finder application.
## 🛠️ Available Tools
### fix_default_locations.py
**Purpose**: Ensures all users have their default location marked as accessible.
**Usage**:
```bash
cd app/admin
python fix_default_locations.py
```
**What it does**:
- Scans all users in `data/users.json`
- Checks if each user's default location is marked as accessible
- Automatically fixes any users where the default location is not accessible
- Reports results for each user
**When to use**:
- After manual edits to users.json
- After importing users from backup
- If users report they can't access their default location
- As a maintenance check
### test_password_security.py
**Purpose**: Demonstrates and tests the password hashing security implementation.
**Usage**:
```bash
cd app/admin
python test_password_security.py
```
**What it does**:
- Demonstrates PBKDF2-SHA256 password hashing
- Shows security features (iterations, salt, etc.)
- Verifies password verification works correctly
- Useful for understanding the security implementation
**When to use**:
- To verify password hashing is working correctly
- To demonstrate security to stakeholders
- For educational purposes
- When troubleshooting password-related issues
## 📋 Running Admin Tools
All admin tools should be run from within the `app/admin` directory to ensure correct file paths.
**Example**:
```bash
# Navigate to admin folder
cd "C:\Users\Work\Desktop\CGW Product Finder\app\admin"
# Run a tool
python fix_default_locations.py
```
## ⚠️ Important Notes
- **Backup First**: Always backup `data/users.json` before running utilities that modify data
- **Test Environment**: Test utilities in a development environment before running in production
- **File Paths**: These tools assume they're being run from the `app/admin` directory
- **Python Version**: Requires Python 3.7 or higher
## 🔒 Security Considerations
- These tools have direct access to user data
- Do not expose these tools to web-facing directories
- Keep this folder secure with appropriate file permissions
- Never commit sensitive user data to version control
## 📝 Adding New Admin Tools
When adding new administrative tools to this folder:
1. Add the Python script file
2. Update this README with documentation
3. Include clear usage instructions
4. Document any data modifications it makes
5. Include error handling and user feedback
## 🐛 Troubleshooting
**"File not found" errors**:
- Ensure you're running from the `app/admin` directory
- Check that `../data/users.json` exists
**Permission denied**:
- Ensure the application is not running
- Check file permissions on `data/users.json`
**Import errors**:
- Ensure all required dependencies are installed: `pip install -r ../requirements.txt`
- Verify Python version: `python --version`
+65
View File
@@ -0,0 +1,65 @@
"""
Utility script to fix existing users by ensuring their default location
is marked as accessible in their locationSettings.
"""
import json
import os
# Get the directory where this script is located
basedir = os.path.abspath(os.path.dirname(__file__))
USERS_FILE = os.path.join(basedir, '..', 'data', 'users.json')
def fix_default_locations():
"""
Ensure all users have their default location marked as accessible.
"""
if not os.path.exists(USERS_FILE):
print("No users.json file found.")
return
# Load users
with open(USERS_FILE, 'r') as f:
users = json.load(f)
print(f"Found {len(users)} users to check...")
changes_made = 0
for user in users:
username = user.get('username', 'Unknown')
default_location = user.get('defaultLocation')
if not default_location:
print(f"⚠️ User '{username}' has no default location - skipping")
continue
location_settings = user.get('locationSettings', {})
# Check if default location is in settings and accessible
if default_location not in location_settings:
print(f"✓ Adding default location '{default_location}' for user '{username}'")
location_settings[default_location] = {'accessible': True}
user['locationSettings'] = location_settings
changes_made += 1
elif not location_settings[default_location].get('accessible', False):
print(f"✓ Marking default location '{default_location}' as accessible for user '{username}'")
location_settings[default_location]['accessible'] = True
changes_made += 1
else:
print(f" User '{username}' - default location already accessible")
if changes_made > 0:
# Save updated users
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)
print(f"\n✓ Fixed {changes_made} user(s) and saved to users.json")
else:
print("\n✓ All users already have correct default location settings")
if __name__ == '__main__':
print("=" * 60)
print("DEFAULT LOCATION FIX UTILITY")
print("=" * 60)
print()
fix_default_locations()
print()
+60
View File
@@ -0,0 +1,60 @@
"""
Test script to demonstrate password hashing and verification
"""
from werkzeug.security import generate_password_hash, check_password_hash
# Example: Creating a hashed password
plain_password = "mySecurePassword123"
hashed_password = generate_password_hash(plain_password, method='pbkdf2:sha256')
print("=" * 60)
print("PASSWORD HASHING DEMONSTRATION")
print("=" * 60)
print()
print(f"Original Password: {plain_password}")
print()
print(f"Hashed Password: {hashed_password}")
print()
print(f"Hash Length: {len(hashed_password)} characters")
print()
print("=" * 60)
print("SECURITY FEATURES")
print("=" * 60)
print("✓ Algorithm: PBKDF2-SHA256")
print("✓ Iterations: 600,000+")
print("✓ Unique salt per password")
print("✓ Cannot be reversed to plain text")
print("✓ Industry-standard security")
print()
print("=" * 60)
print("PASSWORD VERIFICATION TEST")
print("=" * 60)
print()
# Test correct password
correct = check_password_hash(hashed_password, "mySecurePassword123")
print(f"Testing correct password: {correct}")
# Test incorrect password
incorrect = check_password_hash(hashed_password, "wrongPassword")
print(f"Testing incorrect password: {incorrect}")
print()
# Show that the same password produces different hashes (due to unique salts)
print("=" * 60)
print("UNIQUE SALT DEMONSTRATION")
print("=" * 60)
print()
print("Hashing the same password twice produces different hashes:")
print()
hash1 = generate_password_hash("password123", method='pbkdf2:sha256')
hash2 = generate_password_hash("password123", method='pbkdf2:sha256')
print(f"Hash 1: {hash1}")
print(f"Hash 2: {hash2}")
print()
print(f"Are they different? {hash1 != hash2}")
print("Both hashes are valid and will verify correctly!")
print(f"Hash 1 verifies: {check_password_hash(hash1, 'password123')}")
print(f"Hash 2 verifies: {check_password_hash(hash2, 'password123')}")
print()
+995
View File
@@ -0,0 +1,995 @@
from flask import Flask, render_template, send_from_directory, jsonify, request, send_file, session, redirect, url_for
import os
from io import BytesIO
import base64
import json
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import secrets
# Import image generator
try:
from image_generator import ProductImageGenerator, image_to_base64
IMAGE_GENERATOR_AVAILABLE = True
print("✓ Image generation available")
except ImportError as e:
IMAGE_GENERATOR_AVAILABLE = False
print(f"Warning: Could not import image generator: {e}")
print("Image generation endpoints will be disabled.")
except Exception as e:
IMAGE_GENERATOR_AVAILABLE = False
print(f"Error loading image generator: {e}")
print("Image generation endpoints will be disabled.")
# Get the directory where this script is located
basedir = os.path.abspath(os.path.dirname(__file__))
# Initialize Flask with explicit paths
app = Flask(__name__,
template_folder=os.path.join(basedir, 'templates'),
static_folder=os.path.join(basedir, 'static'))
# Configure session - generate a random secret key
app.secret_key = secrets.token_hex(32)
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Configure static folders
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching during development
# Support for subdirectory deployments (e.g., /product-finder/)
# Auto-detect production environment based on Python path
import sys
if 'virtualenv/product-finder' in sys.executable or '/home/bmdwtjuw' in sys.executable:
# Running on production server
app.config['APPLICATION_ROOT'] = '/product-finder'
print("[OK] Production environment detected - using /product-finder")
else:
# Local development
app.config['APPLICATION_ROOT'] = os.environ.get('APPLICATION_ROOT', '/')
print(f"[OK] Development environment - using {app.config['APPLICATION_ROOT']}")
# ===== SUBDIRECTORY DEPLOYMENT MIDDLEWARE =====
class PrefixMiddleware:
"""
Middleware to handle subdirectory deployments.
This ensures Flask knows about the /product-finder prefix when deployed.
"""
def __init__(self, app, prefix=''):
self.app = app
self.prefix = prefix.rstrip('/') # Remove trailing slash if present
def __call__(self, environ, start_response):
# Only apply prefix if not already in SCRIPT_NAME and prefix is set
if self.prefix and self.prefix != '/':
# Check if the URL path starts with our prefix
path = environ.get('PATH_INFO', '')
script_name = environ.get('SCRIPT_NAME', '')
# If prefix not already in SCRIPT_NAME, add it
if not script_name.startswith(self.prefix):
environ['SCRIPT_NAME'] = self.prefix + script_name
# Remove prefix from PATH_INFO if it's there
if path.startswith(self.prefix):
environ['PATH_INFO'] = path[len(self.prefix):]
return self.app(environ, start_response)
# Apply middleware if APPLICATION_ROOT is set to a subdirectory
application_root = app.config.get('APPLICATION_ROOT', '/')
if application_root and application_root != '/':
app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=application_root)
print(f"✓ PrefixMiddleware applied for subdirectory: {application_root}")
# Make base URL available to all templates
@app.context_processor
def inject_base_url():
"""Inject base URL into all templates for relative paths"""
return {
'base_url': request.script_root or '',
'url_for': url_for
}
# ===== AUTHENTICATION HELPERS =====
def login_required(f):
"""Decorator to require login for routes"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('login_page'))
# Check if user is active
users = load_users()
if session['user_id'] >= len(users):
session.clear()
return redirect(url_for('login_page', message='User not found'))
user = users[session['user_id']]
if not user.get('active', True):
session.clear()
return redirect(url_for('login_page', message='Account is inactive'))
return f(*args, **kwargs)
return decorated_function
def get_current_user():
"""Get the currently logged in user"""
if 'user_id' not in session:
return None
users = load_users()
if session['user_id'] >= len(users):
return None
return users[session['user_id']]
def can_user(permission, location=None):
"""
Check if current user has a specific permission.
WordPress-style permission checking.
Args:
permission (str): Permission name (e.g., 'manage_users', 'create_quotes')
location (str, optional): Check permission for specific location.
If None, uses current session location.
If 'global', checks global permissions only.
Returns:
bool: True if user has permission, False otherwise
Examples:
can_user('manage_users') # Check global permission
can_user('create_quotes') # Check at current location
can_user('view_reports', 'LINDS') # Check at specific location
"""
user = get_current_user()
if not user:
return False
# Check if user is active
if not user.get('active', True):
return False
# Check global permissions first
global_permissions = user.get('permissions', {})
if permission in global_permissions:
return global_permissions[permission] is True
# If location is 'global', only check global permissions
if location == 'global':
return False
# Determine which location to check
check_location = location if location else session.get('currentLocation')
if not check_location:
return False
# Check location-specific permissions
location_settings = user.get('locationSettings', {})
if check_location in location_settings:
loc_permissions = location_settings[check_location].get('permissions', {})
if permission in loc_permissions:
return loc_permissions[permission] is True
return False
def user_has_any_permission(permissions, location=None):
"""
Check if user has ANY of the provided permissions.
Args:
permissions (list): List of permission names
location (str, optional): Location to check
Returns:
bool: True if user has at least one permission
"""
return any(can_user(perm, location) for perm in permissions)
def user_has_all_permissions(permissions, location=None):
"""
Check if user has ALL of the provided permissions.
Args:
permissions (list): List of permission names
location (str, optional): Location to check
Returns:
bool: True if user has all permissions
"""
return all(can_user(perm, location) for perm in permissions)
def permission_required(permission, location=None):
"""
Decorator to require specific permission for a route.
Similar to @login_required but checks permissions.
Args:
permission (str): Required permission name
location (str, optional): Location to check permission for
Example:
@app.route('/admin/users')
@login_required
@permission_required('manage_users')
def manage_users_page():
return render_template('user_manager.html')
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not can_user(permission, location):
return render_template('access_denied.html',
required_permission=permission), 403
return f(*args, **kwargs)
return decorated_function
return decorator
def get_user_permissions(location=None):
"""
Get all permissions for the current user.
Args:
location (str, optional): Get permissions for specific location.
If None, returns global + current location.
Returns:
dict: Dictionary of permission_name: True/False
"""
user = get_current_user()
if not user:
return {}
permissions = {}
# Get global permissions
global_perms = user.get('permissions', {})
permissions.update(global_perms)
# Get location-specific permissions
if location != 'global':
check_location = location if location else session.get('currentLocation')
if check_location:
location_settings = user.get('locationSettings', {})
if check_location in location_settings:
loc_perms = location_settings[check_location].get('permissions', {})
permissions.update(loc_perms)
return permissions
# ===== USER MANAGEMENT FUNCTIONS (moved here for use in auth) =====
USERS_FILE = os.path.join(basedir, 'data', 'users.json')
def load_users():
"""Load users from JSON file"""
if os.path.exists(USERS_FILE):
try:
with open(USERS_FILE, 'r') as f:
return json.load(f)
except:
return []
return []
def save_users(users):
"""Save users to JSON file"""
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)
# ===== AUTHENTICATION ROUTES =====
@app.route('/login')
def login_page():
"""Serve the login page"""
# If already logged in, redirect to main app
if 'user_id' in session:
return redirect(url_for('index'))
return render_template('login.html')
@app.route('/select-location')
def select_location_page():
"""Serve the location selection page"""
if 'user_id' not in session:
return redirect(url_for('login_page'))
return render_template('select_location.html')
@app.route('/api/login', methods=['POST'])
def login():
"""Authenticate user and create session"""
try:
data = request.json
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({
'status': 'error',
'message': 'Username and password are required'
}), 400
users = load_users()
# Find user by username
user_index = None
user = None
for i, u in enumerate(users):
if u['username'] == username:
user_index = i
user = u
break
if not user:
return jsonify({
'status': 'error',
'message': 'Invalid username or password'
}), 401
# Check if user is active
if not user.get('active', True):
return jsonify({
'status': 'error',
'message': 'Account is inactive. Please contact an administrator.'
}), 403
# Verify password
if not check_password_hash(user['password'], password):
return jsonify({
'status': 'error',
'message': 'Invalid username or password'
}), 401
# Create session
session['user_id'] = user_index
session['username'] = user['username']
session['defaultLocation'] = user.get('defaultLocation')
# Get accessible locations
accessible_locations = []
if 'locationSettings' in user:
for loc_code, settings in user['locationSettings'].items():
if settings.get('accessible', False):
accessible_locations.append(loc_code)
session['accessibleLocations'] = accessible_locations
# Check if user needs to select a location
requiresLocationSelection = len(accessible_locations) > 1
# If only one location or no accessible locations, auto-select default
if not requiresLocationSelection:
session['currentLocation'] = user.get('defaultLocation')
return jsonify({
'status': 'success',
'message': 'Login successful',
'requiresLocationSelection': requiresLocationSelection
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/logout', methods=['POST'])
def logout():
"""Log out user and clear session"""
session.clear()
return jsonify({
'status': 'success',
'message': 'Logged out successfully'
})
@app.route('/api/session', methods=['GET'])
def get_session():
"""Get current session information"""
if 'user_id' not in session:
return jsonify({
'status': 'error',
'message': 'Not logged in'
}), 401
return jsonify({
'status': 'success',
'username': session.get('username'),
'defaultLocation': session.get('defaultLocation'),
'currentLocation': session.get('currentLocation'),
'accessibleLocations': session.get('accessibleLocations', [])
})
@app.route('/api/select-location', methods=['POST'])
def select_location():
"""Select a location for the current session"""
if 'user_id' not in session:
return jsonify({
'status': 'error',
'message': 'Not logged in'
}), 401
try:
data = request.json
location = data.get('location')
# Verify user has access to this location
accessible = session.get('accessibleLocations', [])
if location not in accessible:
return jsonify({
'status': 'error',
'message': 'You do not have access to this location'
}), 403
session['currentLocation'] = location
return jsonify({
'status': 'success',
'message': 'Location selected',
'currentLocation': location
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
# ===== PERMISSION CHECK ENDPOINT =====
@app.route('/api/check-permission', methods=['POST'])
@login_required
def check_permission():
"""Check if current user has specific permission(s)"""
try:
data = request.json
permission = data.get('permission')
location = data.get('location') # Optional, defaults to current location
if not permission:
return jsonify({
'status': 'error',
'message': 'Permission name required'
}), 400
has_permission = can_user(permission, location)
return jsonify({
'status': 'success',
'hasPermission': has_permission,
'permission': permission,
'location': location if location else session.get('currentLocation', 'global')
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/user-permissions', methods=['GET'])
@login_required
def get_user_permissions_endpoint():
"""Get all permissions for current user"""
try:
location = request.args.get('location') # Optional query parameter
permissions = get_user_permissions(location)
return jsonify({
'status': 'success',
'permissions': permissions,
'location': location if location else session.get('currentLocation', 'global')
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
# ===== MAIN APPLICATION ROUTES =====
@app.route('/')
def index():
"""Serve the quiz page as the main page or redirect to login"""
# Simple test to see if app is running
if 'username' not in session:
return redirect(url_for('login_page'))
# If user hasn't selected a location yet, redirect to selection
if not session.get('currentLocation') and len(session.get('accessibleLocations', [])) > 1:
return redirect(url_for('select_location_page'))
return render_template('index2.html')
@app.route('/test')
def test_route():
"""Simple test route to verify app is running - NO AUTH REQUIRED"""
import sys
info = {
'status': 'OK',
'message': 'Flask app is running!',
'python': sys.version,
'application_root': app.config.get('APPLICATION_ROOT'),
'middleware': type(app.wsgi_app).__name__,
'routes': len(list(app.url_map.iter_rules()))
}
return f"""
<html>
<head><title>App Test</title></head>
<body style="font-family: monospace; padding: 20px;">
<h1>✓ Flask App is Running!</h1>
<ul>
<li><strong>Python:</strong> {info['python']}</li>
<li><strong>APPLICATION_ROOT:</strong> {info['application_root']}</li>
<li><strong>Middleware:</strong> {info['middleware']}</li>
<li><strong>Routes:</strong> {info['routes']}</li>
</ul>
<p><a href="{url_for('login_page')}">Go to Login</a></p>
</body>
</html>
"""
@app.route('/quiz')
@login_required
def quiz():
"""Serve the quiz page"""
return render_template('index2.html')
@app.route('/image-test')
@login_required
def image_test():
"""Serve the image generation test page"""
return render_template('image_test.html')
@app.route('/css/<path:filename>')
def serve_css(filename):
"""Serve CSS files"""
return send_from_directory(os.path.join(basedir, 'css'), filename)
@app.route('/js/<path:filename>')
def serve_js(filename):
"""Serve JavaScript files"""
return send_from_directory(os.path.join(basedir, 'js'), filename)
@app.route('/images/<path:filename>')
def serve_images(filename):
"""Serve image files"""
return send_from_directory(os.path.join(basedir, 'images'), filename)
@app.route('/data/<path:filename>')
def serve_data(filename):
"""Serve data files (JSON)"""
return send_from_directory(os.path.join(basedir, 'data'), filename)
# API endpoint for future expansion (e.g., saving user selections)
@app.route('/api/save-selection', methods=['POST'])
def save_selection():
"""API endpoint to save user selections"""
data = request.json
# Here you could save to a database or file
# For now, just return success
return jsonify({
'status': 'success',
'message': 'Selection saved',
'data': data
})
@app.route('/api/get-products', methods=['GET'])
def get_products():
"""API endpoint to get product data"""
# This could fetch from a database in the future
return jsonify({
'status': 'success',
'products': []
})
# ===== DYNAMIC IMAGE GENERATION ENDPOINTS =====
@app.route('/api/product-image/<product_code>')
def generate_product_image(product_code):
"""
Generate a product image with specified configuration.
URL Parameters:
color: Color name (default: 'white')
hinge: Hinge side - 'left' or 'right' (default: 'right')
material: Material type (default: 'aluminum')
format: Return format - 'image' or 'json' (default: 'image')
Examples:
/api/product-image/cobrai?color=black&hinge=left
/api/product-image/cobrai?color=bronze&hinge=right&format=json
"""
if not IMAGE_GENERATOR_AVAILABLE:
return jsonify({
'error': 'Image generation not available. Please install Pillow: pip install Pillow'
}), 500
try:
# Get parameters
color = request.args.get('color', 'white').lower()
hinge = request.args.get('hinge', 'right').lower()
material = request.args.get('material', 'aluminum').lower()
return_format = request.args.get('format', 'image').lower()
use_cache = request.args.get('cache', 'true').lower() == 'true'
# Initialize generator
generator = ProductImageGenerator()
# Generate image
image = generator.generate_product_image(
product_code=product_code,
color=color,
hinge=hinge,
material=material,
use_cache=use_cache
)
# Return based on format
if return_format == 'json':
# Return base64 encoded image in JSON
img_base64 = image_to_base64(image, format='PNG')
return jsonify({
'status': 'success',
'productCode': product_code,
'color': color,
'hinge': hinge,
'material': material,
'image': img_base64,
'format': 'base64'
})
else:
# Return raw image file
img_io = BytesIO()
image.save(img_io, 'PNG', optimize=True)
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
except ValueError as e:
return jsonify({'error': str(e)}), 404
except FileNotFoundError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': f'Error generating image: {str(e)}'}), 500
@app.route('/api/product-config/<product_code>')
def get_product_image_config(product_code):
"""
Get the image configuration for a product.
Returns JSON with available colors, hardware positions, and other metadata.
Example:
/api/product-config/cobrai
"""
if not IMAGE_GENERATOR_AVAILABLE:
return jsonify({
'error': 'Image generation not available. Please install Pillow: pip install Pillow'
}), 500
try:
generator = ProductImageGenerator()
config = generator.get_product_config(product_code)
if not config:
return jsonify({
'error': f'Configuration for product {product_code} not found'
}), 404
return jsonify({
'status': 'success',
'productCode': product_code,
'config': config
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/clear-image-cache', methods=['POST'])
def clear_image_cache():
"""
Clear the image cache.
Optional JSON body:
{ "productCode": "cobrai" } - Clear cache for specific product
Example:
POST /api/clear-image-cache
POST /api/clear-image-cache with body: {"productCode": "cobrai"}
"""
if not IMAGE_GENERATOR_AVAILABLE:
return jsonify({
'error': 'Image generation not available.'
}), 500
try:
data = request.json or {}
product_code = data.get('productCode')
generator = ProductImageGenerator()
generator.clear_cache(product_code)
return jsonify({
'status': 'success',
'message': f'Cache cleared for {product_code}' if product_code else 'All cache cleared'
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# ===== END DYNAMIC IMAGE GENERATION ENDPOINTS =====
# ===== USER MANAGEMENT ENDPOINTS =====
@app.route('/users')
@login_required
@permission_required('manage_users')
def user_manager():
"""Serve the user management page"""
return render_template('user_manager.html')
@app.route('/api/users', methods=['GET'])
@login_required
@permission_required('manage_users')
def get_users():
"""Get all users"""
users = load_users()
return jsonify({
'status': 'success',
'users': users
})
@app.route('/api/users', methods=['POST'])
@login_required
@permission_required('manage_users')
def add_user():
"""Add a new user with hashed password"""
try:
data = request.json
# Validate required fields
if not data.get('username') or not data.get('password') or not data.get('defaultLocation'):
return jsonify({
'status': 'error',
'message': 'Username, password, and default location are required'
}), 400
users = load_users()
# Check if username already exists
if any(user['username'] == data['username'] for user in users):
return jsonify({
'status': 'error',
'message': 'Username already exists'
}), 400
# Hash the password using pbkdf2:sha256 (secure)
hashed_password = generate_password_hash(data['password'], method='pbkdf2:sha256')
# Get location settings and ensure default location is marked as accessible
location_settings = data.get('locationSettings', {})
default_location = data['defaultLocation']
# Ensure default location exists in settings and is marked as accessible
if default_location not in location_settings:
location_settings[default_location] = {}
location_settings[default_location]['accessible'] = True
# Create new user
new_user = {
'username': data['username'],
'password': hashed_password,
'defaultLocation': default_location,
'locationSettings': location_settings,
'active': data.get('active', True) # Default to active
}
users.append(new_user)
save_users(users)
return jsonify({
'status': 'success',
'message': 'User added successfully',
'users': users
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/users/<int:user_index>/active', methods=['PATCH'])
@login_required
@permission_required('manage_users')
def toggle_user_active(user_index):
"""Toggle user active status"""
try:
data = request.json
users = load_users()
if user_index < 0 or user_index >= len(users):
return jsonify({
'status': 'error',
'message': 'Invalid user index'
}), 400
# Update active status
users[user_index]['active'] = data.get('active', True)
save_users(users)
return jsonify({
'status': 'success',
'message': 'User status updated',
'users': users
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/users/<int:user_index>', methods=['DELETE'])
@login_required
@permission_required('manage_users')
def delete_user(user_index):
"""Delete a specific user by index"""
try:
users = load_users()
if user_index < 0 or user_index >= len(users):
return jsonify({
'status': 'error',
'message': 'Invalid user index'
}), 400
users.pop(user_index)
save_users(users)
return jsonify({
'status': 'success',
'message': 'User deleted successfully',
'users': users
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/users/<int:user_index>/change-password', methods=['POST'])
@login_required
@permission_required('manage_users')
def change_user_password(user_index):
"""Change a user's password with current user verification"""
try:
data = request.json
new_password = data.get('newPassword')
current_user_password = data.get('currentUserPassword')
# Validate required fields
if not new_password or not current_user_password:
return jsonify({
'status': 'error',
'message': 'New password and current user password are required'
}), 400
# Validate password length
if len(new_password) < 6:
return jsonify({
'status': 'error',
'message': 'Password must be at least 6 characters long'
}), 400
# Get current logged-in user
current_user = get_current_user()
if not current_user:
return jsonify({
'status': 'error',
'message': 'Not authenticated'
}), 401
# Verify current user's password
if not check_password_hash(current_user['password'], current_user_password):
return jsonify({
'status': 'error',
'message': 'Your password is incorrect. Password change denied for security reasons.'
}), 403
# Load users and validate index
users = load_users()
if user_index < 0 or user_index >= len(users):
return jsonify({
'status': 'error',
'message': 'Invalid user index'
}), 400
# Hash the new password
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
# Update the target user's password
target_username = users[user_index]['username']
users[user_index]['password'] = hashed_password
save_users(users)
return jsonify({
'status': 'success',
'message': f'Password changed successfully for user "{target_username}"'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/users/clear', methods=['DELETE'])
def clear_all_users():
"""Clear all users"""
try:
save_users([])
return jsonify({
'status': 'success',
'message': 'All users cleared'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/users/download')
def download_users():
"""Download users as JSON file"""
try:
users = load_users()
if not users:
return jsonify({
'status': 'error',
'message': 'No users to download'
}), 400
# Create JSON file in memory
json_data = json.dumps(users, indent=2)
# Send as downloadable file
return send_file(
BytesIO(json_data.encode()),
mimetype='application/json',
as_attachment=True,
download_name='users.json'
)
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
# ===== END USER MANAGEMENT ENDPOINTS =====
# Error handlers
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(e):
return "Internal Server Error", 500
if __name__ == '__main__':
# Create necessary directories
os.makedirs(os.path.join(basedir, 'images'), exist_ok=True)
os.makedirs(os.path.join(basedir, 'templates'), exist_ok=True)
os.makedirs(os.path.join(basedir, 'css'), exist_ok=True)
os.makedirs(os.path.join(basedir, 'js'), exist_ok=True)
os.makedirs(os.path.join(basedir, 'data'), exist_ok=True)
# Run the application
# Set debug=False for production
# Port 8080 is used instead of 5000 (Windows reserves port 5000)
app.run(debug=True, host='0.0.0.0', port=8080)
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

+44
View File
@@ -0,0 +1,44 @@
"""
Configuration file for Flask application
"""
import os
class Config:
"""Base configuration"""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
DEBUG = False
TESTING = False
# Application root for subdirectory deployments
# Defaults to '/' (root) for local development
# Set to '/product-finder' for production deployment
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/')
class DevelopmentConfig(Config):
"""Development configuration"""
DEBUG = True
ENV = 'development'
# Development runs at root
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/')
class ProductionConfig(Config):
"""Production configuration"""
DEBUG = False
ENV = 'production'
# Production deployed to /product-finder subdirectory
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/product-finder')
# Add production-specific settings here
# DATABASE_URI = os.environ.get('DATABASE_URL')
class TestingConfig(Config):
"""Testing configuration"""
TESTING = True
DEBUG = True
# Configuration dictionary
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
+737
View File
@@ -0,0 +1,737 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border: 2px solid #333;
border-radius: 8px;
overflow: hidden;
}
.header {
background-color: #333;
color: white;
padding: 20px;
text-align: center;
position: relative;
}
.user-info-bar {
position: absolute;
top: 10px;
right: 20px;
display: flex;
align-items: center;
gap: 15px;
font-size: 14px;
}
.logout-btn {
background-color: #f56565;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background-color 0.3s;
}
.logout-btn:hover {
background-color: #e53e3e;
}
.question-container {
padding: 30px;
text-align: center;
}
.question-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.question-subtitle {
font-size: 16px;
color: #666;
margin-bottom: 30px;
}
.buttons-wrapper {
max-height: 500px;
overflow-y: auto;
padding: 20px;
border-top: 1px solid #ddd;
}
.buttons-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
}
.answer-button {
width: 180px;
padding: 15px;
background-color: white;
border: 2px solid #333;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.answer-button:hover {
background-color: #f0f0f0;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.answer-button:active {
transform: translateY(0);
}
.answer-image {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
background-color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #999;
}
.answer-caption {
font-size: 14px;
font-weight: bold;
color: #333;
word-wrap: break-word;
}
.prod_code {
display: block;
font-size: 11px;
font-weight: normal;
color: #666;
margin-top: 4px;
font-style: italic;
}
/* Product Configuration Form */
.product-config-form {
margin-top: 20px;
padding: 20px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
text-align: left;
}
.product-config-form h3 {
margin-bottom: 15px;
color: #333;
font-size: 18px;
}
.config-field {
margin-bottom: 15px;
}
.config-field label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: bold;
font-size: 14px;
}
.config-select {
width: 100%;
padding: 8px 12px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
cursor: pointer;
}
.config-select:focus {
outline: none;
border-color: #333;
}
.config-select:disabled {
background-color: #f0f0f0;
color: #666;
cursor: not-allowed;
opacity: 0.7;
}
.config-input {
width: 100%;
padding: 8px 12px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
box-sizing: border-box;
}
.config-input:focus {
outline: none;
border-color: #333;
}
.config-input::placeholder {
color: #999;
}
.hinge-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: normal;
}
.radio-label input[type="radio"] {
margin-right: 8px;
cursor: pointer;
}
.radio-label span {
font-size: 14px;
color: #333;
}
.result-container {
padding: 40px;
text-align: center;
}
.result-title {
font-size: 28px;
font-weight: bold;
color: #333;
margin-bottom: 30px;
}
.result-content {
display: flex;
gap: 30px;
margin-bottom: 30px;
align-items: flex-start;
text-align: left;
}
.result-image-section {
flex: 0 0 350px;
min-width: 350px;
}
.product-image {
width: 100%;
height: auto;
border-radius: 8px;
border: 2px solid #333;
object-fit: contain;
max-height: 400px;
background-color: #f5f5f5;
transition: transform 0.3s ease;
}
/* Layered Image System */
.door-configurator {
position: relative;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
border: 2px solid #333;
overflow: hidden;
background-color: #f5f5f5;
}
.door-configurator img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
}
.layer-base {
position: relative !important;
z-index: 1;
}
.layer-door {
z-index: 2;
}
.layer-hardware {
z-index: 3;
}
.layer-overlay {
z-index: 4;
}
.config-preview-notice {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 152, 0, 0.9);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.result-details {
flex: 1;
font-size: 16px;
color: #666;
line-height: 1.6;
}
.result-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
gap: 10px;
}
.actions-left,
.actions-right {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
/* Responsive layout for smaller screens */
@media (max-width: 768px) {
.result-content {
flex-direction: column;
}
.result-image-section {
flex: 1;
min-width: 100%;
max-width: 100%;
}
}
.back-button {
padding: 12px 30px;
background-color: #333;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.back-button:hover {
background-color: #555;
}
/* Hide share buttons (temporarily disabled) */
.back-button[onclick*="shareCurrentPage"] {
display: none;
}
.breadcrumb {
padding: 15px 30px;
background-color: #f9f9f9;
border-bottom: 1px solid #ddd;
font-size: 14px;
color: #666;
}
.breadcrumb span {
margin: 0 5px;
}
/* Scrollbar styling */
.buttons-wrapper::-webkit-scrollbar {
width: 10px;
}
.buttons-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
}
.buttons-wrapper::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
.buttons-wrapper::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Measurement Type Toggle Buttons */
.measurement-type-container {
display: flex;
gap: 15px;
justify-content: center;
padding: 20px;
margin: 0 auto;
max-width: 500px;
}
.measurement-toggle {
flex: 1;
max-width: 200px;
min-height: 120px;
padding: 20px 15px;
background-color: #f0f0f0;
border: 3px solid #333;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
}
.measurement-toggle:hover:not(.disabled) {
background-color: #e0e0e0;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.measurement-toggle.selected {
background-color: #333;
color: white;
}
.measurement-toggle.selected:hover {
background-color: #444;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.measurement-toggle.selected .toggle-label {
color: white;
}
.measurement-toggle.disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #f5f5f5;
}
.toggle-icon {
font-size: 48px;
line-height: 1;
display: block;
}
.toggle-label {
font-size: 16px;
font-weight: bold;
color: #333;
display: block;
line-height: 1.2;
}
.measurement-toggle.selected .toggle-icon {
filter: grayscale(0%) brightness(200%);
}
/* Form Styles */
.form-wrapper {
max-height: 500px;
overflow-y: auto;
padding: 20px;
border-top: 1px solid #ddd;
}
.dynamic-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="email"],
.form-group input[type="tel"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #333;
border-radius: 4px;
font-size: 14px;
font-family: Arial, sans-serif;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #555;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-group select {
cursor: pointer;
background-color: white;
}
.submit-button {
width: 100%;
padding: 12px 30px;
background-color: #333;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: 10px;
}
.submit-button:hover {
background-color: #555;
}
/* Scrollbar styling for form wrapper */
.form-wrapper::-webkit-scrollbar {
width: 10px;
}
.form-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
}
.form-wrapper::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
.form-wrapper::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Notification animations */
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Notes Section */
.notes-section {
margin: 20px auto;
max-width: 800px;
}
.notes-toggle {
width: 100%;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 12px 16px;
font-size: 14px;
color: #495057;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s ease;
text-align: left;
}
.notes-toggle:hover {
background-color: #e9ecef;
border-color: #ced4da;
}
.notes-icon {
font-size: 18px;
}
.notes-label {
flex: 1;
font-weight: 600;
}
.notes-arrow {
font-size: 12px;
transition: transform 0.2s ease;
}
.notes-content {
margin-top: 10px;
padding: 16px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: #495057;
}
.notes-content a {
color: #007bff;
text-decoration: none;
}
.notes-content a:hover {
text-decoration: underline;
}
.notes-content ul {
margin: 10px 0;
padding-left: 20px;
}
.notes-content li {
margin: 5px 0;
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
/* Accessories list */
.accessories-list {
margin-top: 10px;
}
.accessory-item {
padding: 12px;
margin: 8px 0;
background: #f5f5f5;
border-left: 3px solid #2196F3;
border-radius: 3px;
text-align: left;
}
.accessory-item strong {
color: #333;
display: block;
margin-bottom: 5px;
}
.accessory-item small {
color: #666;
}
/* Product grid for results */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
padding: 20px;
}
.product-card {
background: white;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
text-align: left;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.product-card h3 {
margin-top: 0;
color: #333;
font-size: 1.1em;
margin-bottom: 15px;
}
.product-card p {
margin: 10px 0;
line-height: 1.6;
color: #666;
}
.product-card ul {
list-style-position: inside;
padding-left: 0;
margin-top: 10px;
}
.product-card li {
margin: 5px 0;
color: #666;
}
+1
View File
@@ -0,0 +1 @@
[]
+40
View File
@@ -0,0 +1,40 @@
{
"description": "Bitwise flag system for product classification",
"bit_definitions": {
"base_attributes": {
"bit_0 (1)": "Base Type = Door",
"bit_1 (2)": "Base Type = Window",
"bit_2 (4)": "Is Accessory",
"bit_3 (8)": "Specific Item Link"
},
"materials": {
"bit_4 (16)": "Material = Aluminum",
"bit_5 (32)": "Material = Vinyl"
},
"colors": {
"bit_6 (64)": "Color = Black",
"bit_7 (128)": "Color = White",
"bit_8 (256)": "Color = Bronze",
"bit_9 (512)": "Color = Tan",
"bit_10 (1024)": "Color = Mill",
"bit_11 (2048)": "Color = Sandstone"
},
"subtypes": {
"bit_12 (4096)": "Subtype = Patio Door",
"bit_13 (8192)": "Subtype = Primary Window",
"bit_14 (16384)": "Subtype = Storm Door",
"bit_15 (32768)": "Subtype = Storm Window"
}
},
"usage_examples": {
"check_if_door": "bit_value & 1",
"check_if_window": "bit_value & 2",
"check_if_aluminum": "bit_value & 16",
"check_if_white": "bit_value & 128",
"check_multiple": "(bit_value & 1) and (bit_value & 16) # Door AND Aluminum",
"check_subtype_patio_door": "bit_value & 4096",
"check_subtype_primary_window": "bit_value & 8192",
"check_subtype_storm_door": "bit_value & 16384",
"check_subtype_storm_window": "bit_value & 32768"
}
}
+15
View File
@@ -0,0 +1,15 @@
Product_Name,Image_URL,Product_Type,Notes
Brass Lever,http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg,Hardware,
Brass Lever (White),http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg,Hardware,Also comes in white
Brass Lever (MT Gold),http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg,Hardware,
Brass Lever (550 Gold),http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg,Hardware,
Brass Pull,http://columbiawindows.com/wp-content/uploads/2014/12/dx-ecoat.jpg,Hardware,
Brass Pull (White),http://columbiawindows.com/wp-content/uploads/2014/12/CR3SEC.jpg,Hardware,Also comes in white
Brass Pull (MT Gold),http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg,Hardware,
Brass Pull (550 Gold),http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg,Hardware,
Satin Pull,http://columbiawindows.com/wp-content/uploads/2014/12/dx-satin-finish.jpg,Hardware,
Satin Pull (Silver),http://columbiawindows.com/wp-content/uploads/2014/12/MT-satin-silver.jpg,Hardware,
Black Pull Handle,http://columbiawindows.com/wp-content/uploads/2014/12/VPBLACK.jpg,Hardware,
White Pull Handle,http://columbiawindows.com/wp-content/uploads/2014/12/VPWHITE.jpg,Hardware,
Bevel Cut Insert,http://columbiawindows.com/wp-content/uploads/2014/12/bevel.jpg,Insert,
Brass Kick Panel,http://columbiawindows.com/wp-content/uploads/2014/12/brasskp.jpg,Panel,
1 Product_Name Image_URL Product_Type Notes
2 Brass Lever http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg Hardware
3 Brass Lever (White) http://columbiawindows.com/wp-content/uploads/2014/12/CR2DLEC.jpg Hardware Also comes in white
4 Brass Lever (MT Gold) http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg Hardware
5 Brass Lever (550 Gold) http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg Hardware
6 Brass Pull http://columbiawindows.com/wp-content/uploads/2014/12/dx-ecoat.jpg Hardware
7 Brass Pull (White) http://columbiawindows.com/wp-content/uploads/2014/12/CR3SEC.jpg Hardware Also comes in white
8 Brass Pull (MT Gold) http://columbiawindows.com/wp-content/uploads/2014/12/MT-MET-GOLD.jpg Hardware
9 Brass Pull (550 Gold) http://columbiawindows.com/wp-content/uploads/2014/12/550-met-gold.jpg Hardware
10 Satin Pull http://columbiawindows.com/wp-content/uploads/2014/12/dx-satin-finish.jpg Hardware
11 Satin Pull (Silver) http://columbiawindows.com/wp-content/uploads/2014/12/MT-satin-silver.jpg Hardware
12 Black Pull Handle http://columbiawindows.com/wp-content/uploads/2014/12/VPBLACK.jpg Hardware
13 White Pull Handle http://columbiawindows.com/wp-content/uploads/2014/12/VPWHITE.jpg Hardware
14 Bevel Cut Insert http://columbiawindows.com/wp-content/uploads/2014/12/bevel.jpg Insert
15 Brass Kick Panel http://columbiawindows.com/wp-content/uploads/2014/12/brasskp.jpg Panel
+68
View File
@@ -0,0 +1,68 @@
[
{
"username": "admin_user",
"password": "pbkdf2:sha256:1000000$examplehash$...",
"defaultLocation": "LINDS",
"active": true,
"permissions": {
"manage_users": true,
"view_reports": true,
"create_quotes": true
},
"locationSettings": {
"LINDS": {
"accessible": true,
"permissions": {
"manage_inventory": true,
"approve_quotes": true
}
},
"IOLA": {
"accessible": true,
"permissions": {
"manage_inventory": false,
"approve_quotes": true
}
},
"KC": {
"accessible": false
},
"BMD": {
"accessible": false
}
}
},
{
"username": "standard_user",
"password": "pbkdf2:sha256:1000000$examplehash2$...",
"defaultLocation": "KC",
"active": true,
"permissions": {
"manage_users": false,
"view_reports": true,
"create_quotes": true
},
"locationSettings": {
"LINDS": {
"accessible": true,
"permissions": {
"manage_inventory": false,
"approve_quotes": false
}
},
"IOLA": {
"accessible": true
},
"KC": {
"accessible": true,
"permissions": {
"manage_inventory": false,
"approve_quotes": false
}
},
"BMD": {
"accessible": true
}
}
}
]
+94
View File
@@ -0,0 +1,94 @@
{
"cobrai": {
"sourceImage": "images/cobrai",
"productCode": "COBRAI",
"description": "Storm Door - Full View with Screen",
"availableColors": {
"white": {"rgb": [255, 255, 255], "name": "White"},
"black": {"rgb": [30, 30, 30], "name": "Black"},
"bronze": {"rgb": [110, 80, 50], "name": "Bronze"},
"sandstone": {"rgb": [210, 190, 165], "name": "Sandstone"}
},
"colorableRegions": [
{
"name": "region_name",
"topLeft": [24, 23],
"bottomRight": [505, 1022],
"description": "Width: 481px, Height: 999px"
},
{
"name": "frame",
"topLeft": [0, 0],
"bottomRight": [100, 1450],
"description": "Vertical frame left side"
},
{
"name": "frame_right",
"topLeft": [620, 0],
"bottomRight": [720, 1450],
"description": "Vertical frame right side"
},
{
"name": "top_rail",
"topLeft": [0, 0],
"bottomRight": [720, 80],
"description": "Top horizontal rail"
},
{
"name": "mid_rail",
"topLeft": [0, 590],
"bottomRight": [720, 680],
"description": "Middle horizontal rail"
},
{
"name": "bottom_panel",
"topLeft": [100, 1090],
"bottomRight": [620, 1450],
"description": "Bottom kickplate panel"
}
],
"hardwarePositions": {
"handle_right": {
"x": 580,
"y": 730,
"image": "images/hardware/handle-lever.png"
},
"handle_left": {
"x": 140,
"y": 730,
"image": "images/hardware/handle-lever.png",
"flip": true
},
"lock": {
"x": 360,
"y": 800,
"image": "images/hardware/lock.png"
}
},
"glassRegions": [
{
"name": "top_glass",
"topLeft": [100, 80],
"bottomRight": [620, 590],
"description": "Top screen/glass area"
},
{
"name": "bottom_glass",
"topLeft": [100, 680],
"bottomRight": [620, 1090],
"description": "Bottom screen/glass area"
}
],
"metadata": {
"imageWidth": 720,
"imageHeight": 1450,
"defaultColor": "white",
"defaultHinge": "right"
},
"cache": {
"enabled": true,
"directory": "cache/product_images",
"maxAge": 86400
}
}
}
+461
View File
@@ -0,0 +1,461 @@
{
"start": {
"type": "question",
"inputType": "button",
"title": "What are you looking for?",
"subtitle": "Select the product category",
"notes": "<p><strong>Quick Tips:</strong></p><ul><li>Press <strong>Ctrl+Shift+R</strong> (Windows) or <strong>Cmd+Shift+R</strong> (Mac) to hard refresh the page</li><li>Press <strong>F5</strong> to reload the application</li><li>Use the back button or breadcrumbs to navigate</li></ul>",
"answers": [
{
"caption": "Door",
"image": "\ud83d\udeaa",
"next": "q-door-type",
"filter": {
"baseType": "Door"
}
},
{
"caption": "Window",
"image": "\ud83e\ude9f",
"next": "q-window-type",
"filter": {
"baseType": "Window"
}
}
]
},
"q-door-type": {
"type": "question",
"inputType": "button",
"title": "What type of door?",
"subtitle": "Select the door category",
"answers": [
{
"caption": "Patio Door",
"image": "\ud83d\udeaa",
"next": "q-material-patio-door",
"filter": {
"baseType": "Door",
"subType": "Patio Door"
}
},
{
"caption": "Storm Door",
"image": "\ud83d\udeaa",
"next": "q-color-storm-door-aluminum",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum"
}
}
]
},
"q-material-patio-door": {
"type": "question",
"inputType": "button",
"title": "Select Material",
"subtitle": "Choose your preferred material for Patio Door",
"conditional": {
"type": "material",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Aluminum",
"image": "\ud83d\udd29",
"next": "q-color-patio-door-aluminum",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum"
}
},
{
"caption": "Vinyl",
"image": "\ud83e\ude9f",
"next": "q-color-patio-door-vinyl",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Vinyl"
}
}
]
},
"q-color-patio-door-aluminum": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color for Aluminum Patio Door",
"conditional": {
"type": "color",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Black",
"image": "\u2b1b",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "Black"
}
},
{
"caption": "Bronze",
"image": "\ud83d\udfeb",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "Bronze"
}
},
{
"caption": "Sandstone",
"image": "\ud83d\udfe8",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "Sandstone"
}
},
{
"caption": "White",
"image": "\u2b1c",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "White"
}
}
]
},
"q-color-patio-door-vinyl": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color for Vinyl Patio Door",
"conditional": {
"type": "color",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Tan",
"image": "\ud83d\udfe4",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Vinyl",
"color": "Tan"
}
},
{
"caption": "White",
"image": "\u2b1c",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Vinyl",
"color": "White"
}
}
]
},
"q-color-storm-door-aluminum": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color for Aluminum Storm Door",
"conditional": {
"type": "color",
"fallbackNext": "q-storm-door-sizes"
},
"answers": [
{
"caption": "Black",
"image": "\u2b1b",
"next": "q-storm-door-sizes",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum",
"color": "Black"
}
},
{
"caption": "Bronze",
"image": "\ud83d\udfeb",
"next": "q-storm-door-sizes",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum",
"color": "Bronze"
}
},
{
"caption": "Sandstone",
"image": "\ud83d\udfe8",
"next": "q-storm-door-sizes",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum",
"color": "Sandstone"
}
},
{
"caption": "White",
"image": "\u2b1c",
"next": "q-storm-door-sizes",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum",
"color": "White"
}
}
]
},
"q-window-type": {
"type": "question",
"inputType": "button",
"title": "What type of window?",
"subtitle": "Select the window category",
"answers": [
{
"caption": "Primary Window",
"image": "\ud83e\ude9f",
"next": "q-material-primary-window",
"filter": {
"baseType": "Window",
"subType": "Primary Window"
}
},
{
"caption": "Storm Window",
"image": "\ud83e\ude9f",
"next": "q-color-storm-window-aluminum",
"filter": {
"baseType": "Window",
"subType": "Storm Window",
"material": "Aluminum"
}
}
]
},
"q-material-primary-window": {
"type": "question",
"inputType": "button",
"title": "Select Material",
"subtitle": "Choose your preferred material for Primary Window",
"conditional": {
"type": "material",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Aluminum",
"image": "\ud83d\udd29",
"next": "q-color-primary-window-aluminum",
"filter": {
"baseType": "Window",
"subType": "Primary Window",
"material": "Aluminum"
}
},
{
"caption": "Vinyl",
"image": "\ud83e\ude9f",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Primary Window",
"material": "Vinyl"
}
}
]
},
"q-storm-door-sizes": {
"type": "question",
"inputType": "button",
"title": "Select Storm Door Size",
"subtitle": "Choose a standard size or enter custom dimensions",
"notes": "<p><strong>How to Measure:</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide (PDF)</a>.</p><p><strong>Quick Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
"answers": [
{
"caption": "30\" x 81\"<br /><span class=\"prod_code\">2668</span>",
"image": "📏",
"next": "results",
"dimensions": {
"width": 30,
"height": 81
}
},
{
"caption": "32\" x 81\"<br /><span class=\"prod_code\">2868</span>",
"image": "📏",
"next": "results",
"dimensions": {
"width": 32,
"height": 81
}
},
{
"caption": "36\" x 81\"<br /><span class=\"prod_code\">3068</span>",
"image": "📏",
"next": "results",
"dimensions": {
"width": 36,
"height": 81
}
},
{
"caption": "Custom Size",
"image": "✏️",
"next": "q-dimensions"
}
]
},
"q-color-primary-window-aluminum": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color for Aluminum Primary Window",
"conditional": {
"type": "color",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Bronze",
"image": "\ud83d\udfeb",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Primary Window",
"material": "Aluminum",
"color": "Bronze"
}
},
{
"caption": "White",
"image": "\u2b1c",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Primary Window",
"material": "Aluminum",
"color": "White"
}
}
]
},
"q-color-storm-window-aluminum": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color for Aluminum Storm Window",
"conditional": {
"type": "color",
"fallbackNext": "q-dimensions"
},
"answers": [
{
"caption": "Black",
"image": "\u2b1b",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Storm Window",
"material": "Aluminum",
"color": "Black"
}
},
{
"caption": "Bronze",
"image": "\ud83d\udfeb",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Storm Window",
"material": "Aluminum",
"color": "Bronze"
}
},
{
"caption": "Sandstone",
"image": "\ud83d\udfe8",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Storm Window",
"material": "Aluminum",
"color": "Sandstone"
}
},
{
"caption": "White",
"image": "\u2b1c",
"next": "q-dimensions",
"filter": {
"baseType": "Window",
"subType": "Storm Window",
"material": "Aluminum",
"color": "White"
}
}
]
},
"q-dimensions": {
"type": "question",
"inputType": "form",
"title": "Select Storm Door Size",
"subtitle": "Choose a standard size or enter custom dimensions",
"notes": "<p><strong>How to Measure:</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide (PDF)</a>.</p><p><strong>Quick Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
"measurementType": {
"defaultValue": "opening-size",
"options": [
{
"value": "opening-size",
"label": "Opening Size",
"disabled": false
},
{
"value": "tip-to-tip",
"label": "Tip-to-tip",
"disabled": true
}
]
},
"fields": [
{
"name": "width",
"label": "Width (inches)",
"type": "number",
"required": true,
"placeholder": "26"
},
{
"name": "height",
"label": "Height (inches)",
"type": "number",
"required": true,
"placeholder": "81"
}
],
"next": "results"
}
}
+922
View File
@@ -0,0 +1,922 @@
PROD_CODE,BIT_VALUE,BIT_HEX,BIT_BINARY,DISCONTINUED,FLAGS
1650-10,16,0x10,10000,False,Aluminum
404,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
450,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
606,35282,0x89D2,1000100111010010,False,Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
650,33234,0x81D2,1000000111010010,False,Window | Aluminum | Black | White | Bronze | Subtype:Storm Window
1400,160,0xA0,10100000,False,Vinyl | White
1500,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
1510,160,0xA0,10100000,False,Vinyl | White
1650,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1710,16,0x10,10000,False,Aluminum
2000,8594,0x2192,10000110010010,False,Window | Aluminum | White | Bronze | Subtype:Primary Window
2100,16,0x10,10000,False,Aluminum
2200,16,0x10,10000,False,Aluminum
2650,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2700,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2710,16,0x10,10000,False,Aluminum
3000,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
3100,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
3200,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
3300,400,0x190,110010000,False,Aluminum | White | Bronze
3302,400,0x190,110010000,False,Aluminum | White | Bronze
3303,400,0x190,110010000,False,Aluminum | White | Bronze
3310,400,0x190,110010000,False,Aluminum | White | Bronze
3700,400,0x190,110010000,False,Aluminum | White | Bronze
3710,16,0x10,10000,False,Aluminum
4700,400,0x190,110010000,False,Aluminum | White | Bronze
4710,16,0x10,10000,False,Aluminum
5200,4769,0x12A1,1001010100001,False,Door | Vinyl | White | Tan | Subtype:Patio Door
5700,400,0x190,110010000,False,Aluminum | White | Bronze
6700,16,0x10,10000,False,Aluminum
265010,16,0x10,10000,False,Aluminum
1400SCR,144,0x90,10010000,False,Aluminum | White
1650SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1650VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700CSC,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700CV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700ESC,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700EV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
1700VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2000SCR,400,0x190,110010000,False,Aluminum | White | Bronze
2100EV,400,0x190,110010000,False,Aluminum | White | Bronze
2200PDG,16,0x10,10000,False,Aluminum
2650SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2650VP,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2700CV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2700EV,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2700SCR,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
2700VS,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
3000SCR,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
305INS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
306INS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
3100EV,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
3100SCR,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
310PSCR,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
3700SCR,400,0x190,110010000,False,Aluminum | White | Bronze
3700VP,400,0x190,110010000,False,Aluminum | White | Bronze
404ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
450ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
4700SCR,400,0x190,110010000,False,Aluminum | White | Bronze
606ONE,1488,0x5D0,10111010000,False,Aluminum | Black | White | Bronze | Mill
6100I,16785,0x4191,100000110010001,False,Door | Aluminum | White | Bronze | Subtype:Storm Door
650ONE,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
8100I,16785,0x4191,100000110010001,False,Door | Aluminum | White | Bronze | Subtype:Storm Door
BELMONT,672,0x2A0,1010100000,False,Vinyl | White | Tan
BGI,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
BGI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
BGI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
BGIST,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
C-500,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
C1150,160,0xA0,10100000,False,Vinyl | White
C1621,16,0x10,10000,False,Aluminum
C1622,16,0x10,10000,False,Aluminum
C1626VA,16,0x10,10000,False,Aluminum
C1710,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
C1721,16,0x10,10000,False,Aluminum
C1722,16,0x10,10000,False,Aluminum
C1724,16,0x10,10000,False,Aluminum
C1800,33170,0x8192,1000000110010010,False,Window | Aluminum | White | Bronze | Subtype:Storm Window
C2021,16,0x10,10000,False,Aluminum
C2022,16,0x10,10000,False,Aluminum
C2026,16,0x10,10000,False,Aluminum
C2710,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
C300,16,0x10,10000,False,Aluminum
C3221,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
C3222,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
C3223,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
C3224,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
C3300,400,0x190,110010000,False,Aluminum | White | Bronze
C3700,16,0x10,10000,False,Aluminum
C400,160,0xA0,10100000,False,Vinyl | White
C500,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
C500GL,0,0x0,0,False,None
C500GLP,0,0x0,0,False,None
C500PP,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
C500SCR,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
C500VP,2448,0x990,100110010000,False,Aluminum | White | Bronze | Sandstone
C521,400,0x190,110010000,False,Aluminum | White | Bronze
C522,400,0x190,110010000,False,Aluminum | White | Bronze
C526,400,0x190,110010000,False,Aluminum | White | Bronze
C900,400,0x190,110010000,False,Aluminum | White | Bronze
C900EV,400,0x190,110010000,False,Aluminum | White | Bronze
C900SCR,400,0x190,110010000,False,Aluminum | White | Bronze
C910,16,0x10,10000,False,Aluminum
C910PP,16,0x10,10000,False,Aluminum
C921,400,0x190,110010000,False,Aluminum | White | Bronze
C922,400,0x190,110010000,False,Aluminum | White | Bronze
C924,400,0x190,110010000,False,Aluminum | White | Bronze
C931,400,0x190,110010000,False,Aluminum | White | Bronze
C939,400,0x190,110010000,False,Aluminum | White | Bronze
C940,400,0x190,110010000,False,Aluminum | White | Bronze
C949,400,0x190,110010000,False,Aluminum | White | Bronze
C960,16,0x10,10000,False,Aluminum
COBRAI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
COBRATI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
CRWNFVI,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
CRWNSDI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
D770,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
DSGLASS,0,0x0,0,False,None
DURASEA,0,0x0,0,False,None
EXPAND,16,0x10,10000,False,Aluminum
FULLSCR,16,0x10,10000,False,Aluminum
FVGI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
FVSI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
GOLIATH,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
HERCULE,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
IMPERIL,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
INSGLAS,0,0x0,0,False,None
ISP,464,0x1D0,111010000,False,Aluminum | Black | White | Bronze
IVP,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
JET,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
JSP,400,0x190,110010000,False,Aluminum | White | Bronze
JVP,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
KINGDVI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
KINGFSC,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
KINGI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
KINGSDI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
M1200,4497,0x1191,1000110010001,False,Door | Aluminum | White | Bronze | Subtype:Patio Door
M306,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
OUTSIDE,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
PATIOSC,6609,0x19D1,1100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
PDSCRTT,6609,0x19D1,1100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
PRPDS,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
PRSCR,3984,0xF90,111110010000,False,Aluminum | White | Bronze | Tan | Mill | Sandstone
PRSCR11,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
PSINS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
PWS,4048,0xFD0,111111010000,False,Aluminum | Black | White | Bronze | Tan | Mill | Sandstone
PWSINS,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
R1150,160,0xA0,10100000,False,Vinyl | White
R1400,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
R1500,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
R1510,32,0x20,100000,False,Vinyl
R2000,8594,0x2192,10000110010010,False,Window | Aluminum | White | Bronze | Subtype:Primary Window
R2100,400,0x190,110010000,False,Aluminum | White | Bronze
R2100EV,400,0x190,110010000,False,Aluminum | White | Bronze
R2200,400,0x190,110010000,False,Aluminum | White | Bronze
R300,16,0x10,10000,False,Aluminum
R3302,400,0x190,110010000,False,Aluminum | White | Bronze
R400,160,0xA0,10100000,False,Vinyl | White
R770,8354,0x20A2,10000010100010,False,Window | Vinyl | White | Subtype:Primary Window
R770SCR,16,0x10,10000,False,Aluminum
RCKTRAP,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
REWIRE,16,0x10,10000,False,Aluminum
RNDROCK,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
ROCKET,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
RROCKET,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
SCRI,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
SCRI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
SCRI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
SCRI808,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
SSGLASS,0,0x0,0,False,None
SSSCR,16,0x10,10000,False,Aluminum
TBGI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
TBR,4561,0x11D1,1000111010001,False,Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
TGI,1424,0x590,10110010000,False,Aluminum | White | Bronze | Mill
TGI404,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
TGI606,3536,0xDD0,110111010000,False,Aluminum | Black | White | Bronze | Mill | Sandstone
TGIODD,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
THOR,16401,0x4011,100000000010001,False,Door | Aluminum | Subtype:Storm Door
TVI,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
VKI,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
VP3700,400,0x190,110010000,False,Aluminum | White | Bronze
WINDGAT,672,0x2A0,1010100000,False,Vinyl | White | Tan
ZBARS,2512,0x9D0,100111010000,False,Aluminum | Black | White | Bronze | Sandstone
COBRATK,18897,0x49D1,100100111010001,False,Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
,0,0x0,0,False,None
1 PROD_CODE BIT_VALUE BIT_HEX BIT_BINARY DISCONTINUED FLAGS
2 1650-10 16 0x10 10000 False Aluminum
3 404 35282 0x89D2 1000100111010010 False Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
4 450 35282 0x89D2 1000100111010010 False Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
5 606 35282 0x89D2 1000100111010010 False Window | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Window
6 650 33234 0x81D2 1000000111010010 False Window | Aluminum | Black | White | Bronze | Subtype:Storm Window
7 1400 160 0xA0 10100000 False Vinyl | White
8 1500 8354 0x20A2 10000010100010 False Window | Vinyl | White | Subtype:Primary Window
9 1510 160 0xA0 10100000 False Vinyl | White
10 1650 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
11 1700 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
12 1710 16 0x10 10000 False Aluminum
13 2000 8594 0x2192 10000110010010 False Window | Aluminum | White | Bronze | Subtype:Primary Window
14 2100 16 0x10 10000 False Aluminum
15 2200 16 0x10 10000 False Aluminum
16 2650 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
17 2700 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
18 2710 16 0x10 10000 False Aluminum
19 3000 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
20 3100 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
21 3200 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
22 3300 400 0x190 110010000 False Aluminum | White | Bronze
23 3302 400 0x190 110010000 False Aluminum | White | Bronze
24 3303 400 0x190 110010000 False Aluminum | White | Bronze
25 3310 400 0x190 110010000 False Aluminum | White | Bronze
26 3700 400 0x190 110010000 False Aluminum | White | Bronze
27 3710 16 0x10 10000 False Aluminum
28 4700 400 0x190 110010000 False Aluminum | White | Bronze
29 4710 16 0x10 10000 False Aluminum
30 5200 4769 0x12A1 1001010100001 False Door | Vinyl | White | Tan | Subtype:Patio Door
31 5700 400 0x190 110010000 False Aluminum | White | Bronze
32 6700 16 0x10 10000 False Aluminum
33 265010 16 0x10 10000 False Aluminum
34 1400SCR 144 0x90 10010000 False Aluminum | White
35 1650SCR 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
36 1650VS 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
37 1700CSC 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
38 1700CV 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
39 1700ESC 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
40 1700EV 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
41 1700SCR 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
42 1700VS 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
43 2000SCR 400 0x190 110010000 False Aluminum | White | Bronze
44 2100EV 400 0x190 110010000 False Aluminum | White | Bronze
45 2200PDG 16 0x10 10000 False Aluminum
46 2650SCR 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
47 2650VP 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
48 2700CV 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
49 2700EV 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
50 2700SCR 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
51 2700VS 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
52 3000SCR 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
53 305INS 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
54 306INS 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
55 3100EV 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
56 3100SCR 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
57 310PSCR 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
58 3700SCR 400 0x190 110010000 False Aluminum | White | Bronze
59 3700VP 400 0x190 110010000 False Aluminum | White | Bronze
60 404ONE 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
61 450ONE 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
62 4700SCR 400 0x190 110010000 False Aluminum | White | Bronze
63 606ONE 1488 0x5D0 10111010000 False Aluminum | Black | White | Bronze | Mill
64 6100I 16785 0x4191 100000110010001 False Door | Aluminum | White | Bronze | Subtype:Storm Door
65 650ONE 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
66 8100I 16785 0x4191 100000110010001 False Door | Aluminum | White | Bronze | Subtype:Storm Door
67 BELMONT 672 0x2A0 1010100000 False Vinyl | White | Tan
68 BGI 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
69 BGI404 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
70 BGI606 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
71 BGIST 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
72 C-500 2448 0x990 100110010000 False Aluminum | White | Bronze | Sandstone
73 C1150 160 0xA0 10100000 False Vinyl | White
74 C1621 16 0x10 10000 False Aluminum
75 C1622 16 0x10 10000 False Aluminum
76 C1626VA 16 0x10 10000 False Aluminum
77 C1710 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
78 C1721 16 0x10 10000 False Aluminum
79 C1722 16 0x10 10000 False Aluminum
80 C1724 16 0x10 10000 False Aluminum
81 C1800 33170 0x8192 1000000110010010 False Window | Aluminum | White | Bronze | Subtype:Storm Window
82 C2021 16 0x10 10000 False Aluminum
83 C2022 16 0x10 10000 False Aluminum
84 C2026 16 0x10 10000 False Aluminum
85 C2710 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
86 C300 16 0x10 10000 False Aluminum
87 C3221 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
88 C3222 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
89 C3223 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
90 C3224 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
91 C3300 400 0x190 110010000 False Aluminum | White | Bronze
92 C3700 16 0x10 10000 False Aluminum
93 C400 160 0xA0 10100000 False Vinyl | White
94 C500 2448 0x990 100110010000 False Aluminum | White | Bronze | Sandstone
95 C500GL 0 0x0 0 False None
96 C500GLP 0 0x0 0 False None
97 C500PP 2448 0x990 100110010000 False Aluminum | White | Bronze | Sandstone
98 C500SCR 2448 0x990 100110010000 False Aluminum | White | Bronze | Sandstone
99 C500VP 2448 0x990 100110010000 False Aluminum | White | Bronze | Sandstone
100 C521 400 0x190 110010000 False Aluminum | White | Bronze
101 C522 400 0x190 110010000 False Aluminum | White | Bronze
102 C526 400 0x190 110010000 False Aluminum | White | Bronze
103 C900 400 0x190 110010000 False Aluminum | White | Bronze
104 C900EV 400 0x190 110010000 False Aluminum | White | Bronze
105 C900SCR 400 0x190 110010000 False Aluminum | White | Bronze
106 C910 16 0x10 10000 False Aluminum
107 C910PP 16 0x10 10000 False Aluminum
108 C921 400 0x190 110010000 False Aluminum | White | Bronze
109 C922 400 0x190 110010000 False Aluminum | White | Bronze
110 C924 400 0x190 110010000 False Aluminum | White | Bronze
111 C931 400 0x190 110010000 False Aluminum | White | Bronze
112 C939 400 0x190 110010000 False Aluminum | White | Bronze
113 C940 400 0x190 110010000 False Aluminum | White | Bronze
114 C949 400 0x190 110010000 False Aluminum | White | Bronze
115 C960 16 0x10 10000 False Aluminum
116 COBRAI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
117 COBRATI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
118 CRWNFVI 16401 0x4011 100000000010001 False Door | Aluminum | Subtype:Storm Door
119 CRWNSDI 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
120 D770 8354 0x20A2 10000010100010 False Window | Vinyl | White | Subtype:Primary Window
121 DSGLASS 0 0x0 0 False None
122 DURASEA 0 0x0 0 False None
123 EXPAND 16 0x10 10000 False Aluminum
124 FULLSCR 16 0x10 10000 False Aluminum
125 FVGI 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
126 FVSI 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
127 GOLIATH 16401 0x4011 100000000010001 False Door | Aluminum | Subtype:Storm Door
128 HERCULE 16401 0x4011 100000000010001 False Door | Aluminum | Subtype:Storm Door
129 IMPERIL 4561 0x11D1 1000111010001 False Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
130 INSGLAS 0 0x0 0 False None
131 ISP 464 0x1D0 111010000 False Aluminum | Black | White | Bronze
132 IVP 4561 0x11D1 1000111010001 False Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
133 JET 4497 0x1191 1000110010001 False Door | Aluminum | White | Bronze | Subtype:Patio Door
134 JSP 400 0x190 110010000 False Aluminum | White | Bronze
135 JVP 4497 0x1191 1000110010001 False Door | Aluminum | White | Bronze | Subtype:Patio Door
136 KINGDVI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
137 KINGFSC 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
138 KINGI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
139 KINGSDI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
140 M1200 4497 0x1191 1000110010001 False Door | Aluminum | White | Bronze | Subtype:Patio Door
141 M306 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
142 OUTSIDE 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
143 PATIOSC 6609 0x19D1 1100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
144 PDSCRTT 6609 0x19D1 1100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Patio Door
145 PRPDS 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
146 PRSCR 3984 0xF90 111110010000 False Aluminum | White | Bronze | Tan | Mill | Sandstone
147 PRSCR11 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
148 PSINS 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
149 PWS 4048 0xFD0 111111010000 False Aluminum | Black | White | Bronze | Tan | Mill | Sandstone
150 PWSINS 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
151 R1150 160 0xA0 10100000 False Vinyl | White
152 R1400 8354 0x20A2 10000010100010 False Window | Vinyl | White | Subtype:Primary Window
153 R1500 8354 0x20A2 10000010100010 False Window | Vinyl | White | Subtype:Primary Window
154 R1510 32 0x20 100000 False Vinyl
155 R2000 8594 0x2192 10000110010010 False Window | Aluminum | White | Bronze | Subtype:Primary Window
156 R2100 400 0x190 110010000 False Aluminum | White | Bronze
157 R2100EV 400 0x190 110010000 False Aluminum | White | Bronze
158 R2200 400 0x190 110010000 False Aluminum | White | Bronze
159 R300 16 0x10 10000 False Aluminum
160 R3302 400 0x190 110010000 False Aluminum | White | Bronze
161 R400 160 0xA0 10100000 False Vinyl | White
162 R770 8354 0x20A2 10000010100010 False Window | Vinyl | White | Subtype:Primary Window
163 R770SCR 16 0x10 10000 False Aluminum
164 RCKTRAP 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
165 REWIRE 16 0x10 10000 False Aluminum
166 RNDROCK 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
167 ROCKET 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
168 RROCKET 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
169 SCRI 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
170 SCRI404 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
171 SCRI606 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
172 SCRI808 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
173 SSGLASS 0 0x0 0 False None
174 SSSCR 16 0x10 10000 False Aluminum
175 TBGI 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
176 TBR 4561 0x11D1 1000111010001 False Door | Aluminum | Black | White | Bronze | Subtype:Patio Door
177 TGI 1424 0x590 10110010000 False Aluminum | White | Bronze | Mill
178 TGI404 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
179 TGI606 3536 0xDD0 110111010000 False Aluminum | Black | White | Bronze | Mill | Sandstone
180 TGIODD 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
181 THOR 16401 0x4011 100000000010001 False Door | Aluminum | Subtype:Storm Door
182 TVI 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
183 VKI 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
184 VP3700 400 0x190 110010000 False Aluminum | White | Bronze
185 WINDGAT 672 0x2A0 1010100000 False Vinyl | White | Tan
186 ZBARS 2512 0x9D0 100111010000 False Aluminum | Black | White | Bronze | Sandstone
187 COBRATK 18897 0x49D1 100100111010001 False Door | Aluminum | Black | White | Bronze | Sandstone | Subtype:Storm Door
188 0 0x0 0 False None
189 0 0x0 0 False None
190 0 0x0 0 False None
191 0 0x0 0 False None
192 0 0x0 0 False None
193 0 0x0 0 False None
194 0 0x0 0 False None
195 0 0x0 0 False None
196 0 0x0 0 False None
197 0 0x0 0 False None
198 0 0x0 0 False None
199 0 0x0 0 False None
200 0 0x0 0 False None
201 0 0x0 0 False None
202 0 0x0 0 False None
203 0 0x0 0 False None
204 0 0x0 0 False None
205 0 0x0 0 False None
206 0 0x0 0 False None
207 0 0x0 0 False None
208 0 0x0 0 False None
209 0 0x0 0 False None
210 0 0x0 0 False None
211 0 0x0 0 False None
212 0 0x0 0 False None
213 0 0x0 0 False None
214 0 0x0 0 False None
215 0 0x0 0 False None
216 0 0x0 0 False None
217 0 0x0 0 False None
218 0 0x0 0 False None
219 0 0x0 0 False None
220 0 0x0 0 False None
221 0 0x0 0 False None
222 0 0x0 0 False None
223 0 0x0 0 False None
224 0 0x0 0 False None
225 0 0x0 0 False None
226 0 0x0 0 False None
227 0 0x0 0 False None
228 0 0x0 0 False None
229 0 0x0 0 False None
230 0 0x0 0 False None
231 0 0x0 0 False None
232 0 0x0 0 False None
233 0 0x0 0 False None
234 0 0x0 0 False None
235 0 0x0 0 False None
236 0 0x0 0 False None
237 0 0x0 0 False None
238 0 0x0 0 False None
239 0 0x0 0 False None
240 0 0x0 0 False None
241 0 0x0 0 False None
242 0 0x0 0 False None
243 0 0x0 0 False None
244 0 0x0 0 False None
245 0 0x0 0 False None
246 0 0x0 0 False None
247 0 0x0 0 False None
248 0 0x0 0 False None
249 0 0x0 0 False None
250 0 0x0 0 False None
251 0 0x0 0 False None
252 0 0x0 0 False None
253 0 0x0 0 False None
254 0 0x0 0 False None
255 0 0x0 0 False None
256 0 0x0 0 False None
257 0 0x0 0 False None
258 0 0x0 0 False None
259 0 0x0 0 False None
260 0 0x0 0 False None
261 0 0x0 0 False None
262 0 0x0 0 False None
263 0 0x0 0 False None
264 0 0x0 0 False None
265 0 0x0 0 False None
266 0 0x0 0 False None
267 0 0x0 0 False None
268 0 0x0 0 False None
269 0 0x0 0 False None
270 0 0x0 0 False None
271 0 0x0 0 False None
272 0 0x0 0 False None
273 0 0x0 0 False None
274 0 0x0 0 False None
275 0 0x0 0 False None
276 0 0x0 0 False None
277 0 0x0 0 False None
278 0 0x0 0 False None
279 0 0x0 0 False None
280 0 0x0 0 False None
281 0 0x0 0 False None
282 0 0x0 0 False None
283 0 0x0 0 False None
284 0 0x0 0 False None
285 0 0x0 0 False None
286 0 0x0 0 False None
287 0 0x0 0 False None
288 0 0x0 0 False None
289 0 0x0 0 False None
290 0 0x0 0 False None
291 0 0x0 0 False None
292 0 0x0 0 False None
293 0 0x0 0 False None
294 0 0x0 0 False None
295 0 0x0 0 False None
296 0 0x0 0 False None
297 0 0x0 0 False None
298 0 0x0 0 False None
299 0 0x0 0 False None
300 0 0x0 0 False None
301 0 0x0 0 False None
302 0 0x0 0 False None
303 0 0x0 0 False None
304 0 0x0 0 False None
305 0 0x0 0 False None
306 0 0x0 0 False None
307 0 0x0 0 False None
308 0 0x0 0 False None
309 0 0x0 0 False None
310 0 0x0 0 False None
311 0 0x0 0 False None
312 0 0x0 0 False None
313 0 0x0 0 False None
314 0 0x0 0 False None
315 0 0x0 0 False None
316 0 0x0 0 False None
317 0 0x0 0 False None
318 0 0x0 0 False None
319 0 0x0 0 False None
320 0 0x0 0 False None
321 0 0x0 0 False None
322 0 0x0 0 False None
323 0 0x0 0 False None
324 0 0x0 0 False None
325 0 0x0 0 False None
326 0 0x0 0 False None
327 0 0x0 0 False None
328 0 0x0 0 False None
329 0 0x0 0 False None
330 0 0x0 0 False None
331 0 0x0 0 False None
332 0 0x0 0 False None
333 0 0x0 0 False None
334 0 0x0 0 False None
335 0 0x0 0 False None
336 0 0x0 0 False None
337 0 0x0 0 False None
338 0 0x0 0 False None
339 0 0x0 0 False None
340 0 0x0 0 False None
341 0 0x0 0 False None
342 0 0x0 0 False None
343 0 0x0 0 False None
344 0 0x0 0 False None
345 0 0x0 0 False None
346 0 0x0 0 False None
347 0 0x0 0 False None
348 0 0x0 0 False None
349 0 0x0 0 False None
350 0 0x0 0 False None
351 0 0x0 0 False None
352 0 0x0 0 False None
353 0 0x0 0 False None
354 0 0x0 0 False None
355 0 0x0 0 False None
356 0 0x0 0 False None
357 0 0x0 0 False None
358 0 0x0 0 False None
359 0 0x0 0 False None
360 0 0x0 0 False None
361 0 0x0 0 False None
362 0 0x0 0 False None
363 0 0x0 0 False None
364 0 0x0 0 False None
365 0 0x0 0 False None
366 0 0x0 0 False None
367 0 0x0 0 False None
368 0 0x0 0 False None
369 0 0x0 0 False None
370 0 0x0 0 False None
371 0 0x0 0 False None
372 0 0x0 0 False None
373 0 0x0 0 False None
374 0 0x0 0 False None
375 0 0x0 0 False None
376 0 0x0 0 False None
377 0 0x0 0 False None
378 0 0x0 0 False None
379 0 0x0 0 False None
380 0 0x0 0 False None
381 0 0x0 0 False None
382 0 0x0 0 False None
383 0 0x0 0 False None
384 0 0x0 0 False None
385 0 0x0 0 False None
386 0 0x0 0 False None
387 0 0x0 0 False None
388 0 0x0 0 False None
389 0 0x0 0 False None
390 0 0x0 0 False None
391 0 0x0 0 False None
392 0 0x0 0 False None
393 0 0x0 0 False None
394 0 0x0 0 False None
395 0 0x0 0 False None
396 0 0x0 0 False None
397 0 0x0 0 False None
398 0 0x0 0 False None
399 0 0x0 0 False None
400 0 0x0 0 False None
401 0 0x0 0 False None
402 0 0x0 0 False None
403 0 0x0 0 False None
404 0 0x0 0 False None
405 0 0x0 0 False None
406 0 0x0 0 False None
407 0 0x0 0 False None
408 0 0x0 0 False None
409 0 0x0 0 False None
410 0 0x0 0 False None
411 0 0x0 0 False None
412 0 0x0 0 False None
413 0 0x0 0 False None
414 0 0x0 0 False None
415 0 0x0 0 False None
416 0 0x0 0 False None
417 0 0x0 0 False None
418 0 0x0 0 False None
419 0 0x0 0 False None
420 0 0x0 0 False None
421 0 0x0 0 False None
422 0 0x0 0 False None
423 0 0x0 0 False None
424 0 0x0 0 False None
425 0 0x0 0 False None
426 0 0x0 0 False None
427 0 0x0 0 False None
428 0 0x0 0 False None
429 0 0x0 0 False None
430 0 0x0 0 False None
431 0 0x0 0 False None
432 0 0x0 0 False None
433 0 0x0 0 False None
434 0 0x0 0 False None
435 0 0x0 0 False None
436 0 0x0 0 False None
437 0 0x0 0 False None
438 0 0x0 0 False None
439 0 0x0 0 False None
440 0 0x0 0 False None
441 0 0x0 0 False None
442 0 0x0 0 False None
443 0 0x0 0 False None
444 0 0x0 0 False None
445 0 0x0 0 False None
446 0 0x0 0 False None
447 0 0x0 0 False None
448 0 0x0 0 False None
449 0 0x0 0 False None
450 0 0x0 0 False None
451 0 0x0 0 False None
452 0 0x0 0 False None
453 0 0x0 0 False None
454 0 0x0 0 False None
455 0 0x0 0 False None
456 0 0x0 0 False None
457 0 0x0 0 False None
458 0 0x0 0 False None
459 0 0x0 0 False None
460 0 0x0 0 False None
461 0 0x0 0 False None
462 0 0x0 0 False None
463 0 0x0 0 False None
464 0 0x0 0 False None
465 0 0x0 0 False None
466 0 0x0 0 False None
467 0 0x0 0 False None
468 0 0x0 0 False None
469 0 0x0 0 False None
470 0 0x0 0 False None
471 0 0x0 0 False None
472 0 0x0 0 False None
473 0 0x0 0 False None
474 0 0x0 0 False None
475 0 0x0 0 False None
476 0 0x0 0 False None
477 0 0x0 0 False None
478 0 0x0 0 False None
479 0 0x0 0 False None
480 0 0x0 0 False None
481 0 0x0 0 False None
482 0 0x0 0 False None
483 0 0x0 0 False None
484 0 0x0 0 False None
485 0 0x0 0 False None
486 0 0x0 0 False None
487 0 0x0 0 False None
488 0 0x0 0 False None
489 0 0x0 0 False None
490 0 0x0 0 False None
491 0 0x0 0 False None
492 0 0x0 0 False None
493 0 0x0 0 False None
494 0 0x0 0 False None
495 0 0x0 0 False None
496 0 0x0 0 False None
497 0 0x0 0 False None
498 0 0x0 0 False None
499 0 0x0 0 False None
500 0 0x0 0 False None
501 0 0x0 0 False None
502 0 0x0 0 False None
503 0 0x0 0 False None
504 0 0x0 0 False None
505 0 0x0 0 False None
506 0 0x0 0 False None
507 0 0x0 0 False None
508 0 0x0 0 False None
509 0 0x0 0 False None
510 0 0x0 0 False None
511 0 0x0 0 False None
512 0 0x0 0 False None
513 0 0x0 0 False None
514 0 0x0 0 False None
515 0 0x0 0 False None
516 0 0x0 0 False None
517 0 0x0 0 False None
518 0 0x0 0 False None
519 0 0x0 0 False None
520 0 0x0 0 False None
521 0 0x0 0 False None
522 0 0x0 0 False None
523 0 0x0 0 False None
524 0 0x0 0 False None
525 0 0x0 0 False None
526 0 0x0 0 False None
527 0 0x0 0 False None
528 0 0x0 0 False None
529 0 0x0 0 False None
530 0 0x0 0 False None
531 0 0x0 0 False None
532 0 0x0 0 False None
533 0 0x0 0 False None
534 0 0x0 0 False None
535 0 0x0 0 False None
536 0 0x0 0 False None
537 0 0x0 0 False None
538 0 0x0 0 False None
539 0 0x0 0 False None
540 0 0x0 0 False None
541 0 0x0 0 False None
542 0 0x0 0 False None
543 0 0x0 0 False None
544 0 0x0 0 False None
545 0 0x0 0 False None
546 0 0x0 0 False None
547 0 0x0 0 False None
548 0 0x0 0 False None
549 0 0x0 0 False None
550 0 0x0 0 False None
551 0 0x0 0 False None
552 0 0x0 0 False None
553 0 0x0 0 False None
554 0 0x0 0 False None
555 0 0x0 0 False None
556 0 0x0 0 False None
557 0 0x0 0 False None
558 0 0x0 0 False None
559 0 0x0 0 False None
560 0 0x0 0 False None
561 0 0x0 0 False None
562 0 0x0 0 False None
563 0 0x0 0 False None
564 0 0x0 0 False None
565 0 0x0 0 False None
566 0 0x0 0 False None
567 0 0x0 0 False None
568 0 0x0 0 False None
569 0 0x0 0 False None
570 0 0x0 0 False None
571 0 0x0 0 False None
572 0 0x0 0 False None
573 0 0x0 0 False None
574 0 0x0 0 False None
575 0 0x0 0 False None
576 0 0x0 0 False None
577 0 0x0 0 False None
578 0 0x0 0 False None
579 0 0x0 0 False None
580 0 0x0 0 False None
581 0 0x0 0 False None
582 0 0x0 0 False None
583 0 0x0 0 False None
584 0 0x0 0 False None
585 0 0x0 0 False None
586 0 0x0 0 False None
587 0 0x0 0 False None
588 0 0x0 0 False None
589 0 0x0 0 False None
590 0 0x0 0 False None
591 0 0x0 0 False None
592 0 0x0 0 False None
593 0 0x0 0 False None
594 0 0x0 0 False None
595 0 0x0 0 False None
596 0 0x0 0 False None
597 0 0x0 0 False None
598 0 0x0 0 False None
599 0 0x0 0 False None
600 0 0x0 0 False None
601 0 0x0 0 False None
602 0 0x0 0 False None
603 0 0x0 0 False None
604 0 0x0 0 False None
605 0 0x0 0 False None
606 0 0x0 0 False None
607 0 0x0 0 False None
608 0 0x0 0 False None
609 0 0x0 0 False None
610 0 0x0 0 False None
611 0 0x0 0 False None
612 0 0x0 0 False None
613 0 0x0 0 False None
614 0 0x0 0 False None
615 0 0x0 0 False None
616 0 0x0 0 False None
617 0 0x0 0 False None
618 0 0x0 0 False None
619 0 0x0 0 False None
620 0 0x0 0 False None
621 0 0x0 0 False None
622 0 0x0 0 False None
623 0 0x0 0 False None
624 0 0x0 0 False None
625 0 0x0 0 False None
626 0 0x0 0 False None
627 0 0x0 0 False None
628 0 0x0 0 False None
629 0 0x0 0 False None
630 0 0x0 0 False None
631 0 0x0 0 False None
632 0 0x0 0 False None
633 0 0x0 0 False None
634 0 0x0 0 False None
635 0 0x0 0 False None
636 0 0x0 0 False None
637 0 0x0 0 False None
638 0 0x0 0 False None
639 0 0x0 0 False None
640 0 0x0 0 False None
641 0 0x0 0 False None
642 0 0x0 0 False None
643 0 0x0 0 False None
644 0 0x0 0 False None
645 0 0x0 0 False None
646 0 0x0 0 False None
647 0 0x0 0 False None
648 0 0x0 0 False None
649 0 0x0 0 False None
650 0 0x0 0 False None
651 0 0x0 0 False None
652 0 0x0 0 False None
653 0 0x0 0 False None
654 0 0x0 0 False None
655 0 0x0 0 False None
656 0 0x0 0 False None
657 0 0x0 0 False None
658 0 0x0 0 False None
659 0 0x0 0 False None
660 0 0x0 0 False None
661 0 0x0 0 False None
662 0 0x0 0 False None
663 0 0x0 0 False None
664 0 0x0 0 False None
665 0 0x0 0 False None
666 0 0x0 0 False None
667 0 0x0 0 False None
668 0 0x0 0 False None
669 0 0x0 0 False None
670 0 0x0 0 False None
671 0 0x0 0 False None
672 0 0x0 0 False None
673 0 0x0 0 False None
674 0 0x0 0 False None
675 0 0x0 0 False None
676 0 0x0 0 False None
677 0 0x0 0 False None
678 0 0x0 0 False None
679 0 0x0 0 False None
680 0 0x0 0 False None
681 0 0x0 0 False None
682 0 0x0 0 False None
683 0 0x0 0 False None
684 0 0x0 0 False None
685 0 0x0 0 False None
686 0 0x0 0 False None
687 0 0x0 0 False None
688 0 0x0 0 False None
689 0 0x0 0 False None
690 0 0x0 0 False None
691 0 0x0 0 False None
692 0 0x0 0 False None
693 0 0x0 0 False None
694 0 0x0 0 False None
695 0 0x0 0 False None
696 0 0x0 0 False None
697 0 0x0 0 False None
698 0 0x0 0 False None
699 0 0x0 0 False None
700 0 0x0 0 False None
701 0 0x0 0 False None
702 0 0x0 0 False None
703 0 0x0 0 False None
704 0 0x0 0 False None
705 0 0x0 0 False None
706 0 0x0 0 False None
707 0 0x0 0 False None
708 0 0x0 0 False None
709 0 0x0 0 False None
710 0 0x0 0 False None
711 0 0x0 0 False None
712 0 0x0 0 False None
713 0 0x0 0 False None
714 0 0x0 0 False None
715 0 0x0 0 False None
716 0 0x0 0 False None
717 0 0x0 0 False None
718 0 0x0 0 False None
719 0 0x0 0 False None
720 0 0x0 0 False None
721 0 0x0 0 False None
722 0 0x0 0 False None
723 0 0x0 0 False None
724 0 0x0 0 False None
725 0 0x0 0 False None
726 0 0x0 0 False None
727 0 0x0 0 False None
728 0 0x0 0 False None
729 0 0x0 0 False None
730 0 0x0 0 False None
731 0 0x0 0 False None
732 0 0x0 0 False None
733 0 0x0 0 False None
734 0 0x0 0 False None
735 0 0x0 0 False None
736 0 0x0 0 False None
737 0 0x0 0 False None
738 0 0x0 0 False None
739 0 0x0 0 False None
740 0 0x0 0 False None
741 0 0x0 0 False None
742 0 0x0 0 False None
743 0 0x0 0 False None
744 0 0x0 0 False None
745 0 0x0 0 False None
746 0 0x0 0 False None
747 0 0x0 0 False None
748 0 0x0 0 False None
749 0 0x0 0 False None
750 0 0x0 0 False None
751 0 0x0 0 False None
752 0 0x0 0 False None
753 0 0x0 0 False None
754 0 0x0 0 False None
755 0 0x0 0 False None
756 0 0x0 0 False None
757 0 0x0 0 False None
758 0 0x0 0 False None
759 0 0x0 0 False None
760 0 0x0 0 False None
761 0 0x0 0 False None
762 0 0x0 0 False None
763 0 0x0 0 False None
764 0 0x0 0 False None
765 0 0x0 0 False None
766 0 0x0 0 False None
767 0 0x0 0 False None
768 0 0x0 0 False None
769 0 0x0 0 False None
770 0 0x0 0 False None
771 0 0x0 0 False None
772 0 0x0 0 False None
773 0 0x0 0 False None
774 0 0x0 0 False None
775 0 0x0 0 False None
776 0 0x0 0 False None
777 0 0x0 0 False None
778 0 0x0 0 False None
779 0 0x0 0 False None
780 0 0x0 0 False None
781 0 0x0 0 False None
782 0 0x0 0 False None
783 0 0x0 0 False None
784 0 0x0 0 False None
785 0 0x0 0 False None
786 0 0x0 0 False None
787 0 0x0 0 False None
788 0 0x0 0 False None
789 0 0x0 0 False None
790 0 0x0 0 False None
791 0 0x0 0 False None
792 0 0x0 0 False None
793 0 0x0 0 False None
794 0 0x0 0 False None
795 0 0x0 0 False None
796 0 0x0 0 False None
797 0 0x0 0 False None
798 0 0x0 0 False None
799 0 0x0 0 False None
800 0 0x0 0 False None
801 0 0x0 0 False None
802 0 0x0 0 False None
803 0 0x0 0 False None
804 0 0x0 0 False None
805 0 0x0 0 False None
806 0 0x0 0 False None
807 0 0x0 0 False None
808 0 0x0 0 False None
809 0 0x0 0 False None
810 0 0x0 0 False None
811 0 0x0 0 False None
812 0 0x0 0 False None
813 0 0x0 0 False None
814 0 0x0 0 False None
815 0 0x0 0 False None
816 0 0x0 0 False None
817 0 0x0 0 False None
818 0 0x0 0 False None
819 0 0x0 0 False None
820 0 0x0 0 False None
821 0 0x0 0 False None
822 0 0x0 0 False None
823 0 0x0 0 False None
824 0 0x0 0 False None
825 0 0x0 0 False None
826 0 0x0 0 False None
827 0 0x0 0 False None
828 0 0x0 0 False None
829 0 0x0 0 False None
830 0 0x0 0 False None
831 0 0x0 0 False None
832 0 0x0 0 False None
833 0 0x0 0 False None
834 0 0x0 0 False None
835 0 0x0 0 False None
836 0 0x0 0 False None
837 0 0x0 0 False None
838 0 0x0 0 False None
839 0 0x0 0 False None
840 0 0x0 0 False None
841 0 0x0 0 False None
842 0 0x0 0 False None
843 0 0x0 0 False None
844 0 0x0 0 False None
845 0 0x0 0 False None
846 0 0x0 0 False None
847 0 0x0 0 False None
848 0 0x0 0 False None
849 0 0x0 0 False None
850 0 0x0 0 False None
851 0 0x0 0 False None
852 0 0x0 0 False None
853 0 0x0 0 False None
854 0 0x0 0 False None
855 0 0x0 0 False None
856 0 0x0 0 False None
857 0 0x0 0 False None
858 0 0x0 0 False None
859 0 0x0 0 False None
860 0 0x0 0 False None
861 0 0x0 0 False None
862 0 0x0 0 False None
863 0 0x0 0 False None
864 0 0x0 0 False None
865 0 0x0 0 False None
866 0 0x0 0 False None
867 0 0x0 0 False None
868 0 0x0 0 False None
869 0 0x0 0 False None
870 0 0x0 0 False None
871 0 0x0 0 False None
872 0 0x0 0 False None
873 0 0x0 0 False None
874 0 0x0 0 False None
875 0 0x0 0 False None
876 0 0x0 0 False None
877 0 0x0 0 False None
878 0 0x0 0 False None
879 0 0x0 0 False None
880 0 0x0 0 False None
881 0 0x0 0 False None
882 0 0x0 0 False None
883 0 0x0 0 False None
884 0 0x0 0 False None
885 0 0x0 0 False None
886 0 0x0 0 False None
887 0 0x0 0 False None
888 0 0x0 0 False None
889 0 0x0 0 False None
890 0 0x0 0 False None
891 0 0x0 0 False None
892 0 0x0 0 False None
893 0 0x0 0 False None
894 0 0x0 0 False None
895 0 0x0 0 False None
896 0 0x0 0 False None
897 0 0x0 0 False None
898 0 0x0 0 False None
899 0 0x0 0 False None
900 0 0x0 0 False None
901 0 0x0 0 False None
902 0 0x0 0 False None
903 0 0x0 0 False None
904 0 0x0 0 False None
905 0 0x0 0 False None
906 0 0x0 0 False None
907 0 0x0 0 False None
908 0 0x0 0 False None
909 0 0x0 0 False None
910 0 0x0 0 False None
911 0 0x0 0 False None
912 0 0x0 0 False None
913 0 0x0 0 False None
914 0 0x0 0 False None
915 0 0x0 0 False None
916 0 0x0 0 False None
917 0 0x0 0 False None
918 0 0x0 0 False None
919 0 0x0 0 False None
920 0 0x0 0 False None
921 0 0x0 0 False None
922 0 0x0 0 False None
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"COBRAI": "1100",
"COBRATI": "1100",
"TVI": "1100",
"TRTI": "1100",
"1400": "1400",
"1500": "1500",
"1650": "1650",
"2000": "2000",
"1700": "1700",
"2100": "2100",
"2700": "2700",
"3100": "3100",
"1510": "1510",
"2200": "2200",
"3200": "3200",
"3300": "3300",
"3302": "3302",
"3303": "3303",
"3310": "3310",
"2400": "2400"
}
File diff suppressed because it is too large Load Diff
+998
View File
@@ -0,0 +1,998 @@
,,,,,,Base,Sub-type,,Accessory,,Materials,,Colors,,,,,,
DISCONT,LOC_CODE,PROD_CODE,CATEGORY,CATEGORY_NEW,DESCRIPTION,Type,Door,Window,Yes,This Item,Aluminum,Vinyl,Black,White,Bronze,Tan,Mill,Sandstone,~
FALSE,Iola,1650-10,FPPW,,#1650-10 INSULATED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,303,STORMS,,#303 DART STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,350,STORMS,,#350 ARROW STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,404,STORMS,,#404 FALCON STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,450,STORMS,,#450 RAVEN STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,606,STORMS,,#606 LION STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,650,STORMS,,#650 LYNX STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,808,STORMS,,808 HAWK STORM WINDOW,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,850,STORMS,,#850 KENT STORM WINDOWS,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1400,SHPW,,SERIES 1400 VINYL SLIDING PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1500,SHPT,,SERIES 1500 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1510,FPPW,,1510 VINYL INSULATED FIXED LITE,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1650,SHPW,,#1650 INSULATED SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700,SHPW,,#1700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1710,FPPW,,C-1710 FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,2000,SHPT,,SERIES 2000 S.H. T.B. TILT PRIMARY,Window,,Primary Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,2100,SHPT,,SERIES 2100 THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,2200,FPPW,,SERIES 2200 T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,2400,PD,,2400 ROYAL CROWN PATIO DOOR,Door,Patio Door,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,2650,SHPW,,#2650 SINGLE HUNG SINGLE GLAZED PRIME,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2700,SHPW,,#2700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2710,FPPW,,C-2710 FIXED LITE - SINGLE GLAZED,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,3000,3000DHP,#N/A,SERIES 3000 D.H. T.B REPLACEMENT WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3100,3000DHP,#N/A,SERIES 3100 T.B. REPLACEMENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3200,3000FPP,#N/A,SERIES 3200 T.B. PICTURE WINDOW,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3300,CASEMNT,,3300 1-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3302,CASEMNT,,3300 2-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3303,CASEMNT,,3300 3-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3310,CASEMNT,,3310 FIXED CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,3400,CASEMNT,,3400 1 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,3402,CASEMNT,,3400 2 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,3403,CASEMNT,,3400 3 PANEL VINYL CLAD CASEMENT,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,3500,CASEMNT,,3500 1-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,3502,CASEMNT,,3500 2-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,3503,CASEMNT,,3503 3-PANEL WOOD INTERIOR CASEMENT,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,3700,SHPW,,#3700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3710,FPPW,,C-3710 INSULATED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,4100,REPLACE,,SERIES 4100 VINYL SLIDER,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,4700,SHPW,,#4700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,4710,FPPW,,#4710 SINGLE GLAZED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,5200,PD,,5200 VINYL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
FALSE,Iola,5700,SHPW,,#5700 INSULATED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,6300,SHPT,,SERIES 6300 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,6301,SHPW,,SERIES 6301 VINYL SLIDING PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,6700,SHPW,,#6700 SINGLE GLAZED SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,265010,FPPW,,#2650-10 SINGLE GLAZED FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1400SCR,SHPW,,1400 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,1650SCR,SCREENS,,SCREENS FOR #1650,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1650VS,SHPW,,1650 BOTTOM SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700CSC,SCREENS,,SCREEN FOR #1700 CENTER VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700CV,SHPW,,1700 CENTER VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700ESC,SCREENS,,SCREEN FOR #1700 ENDS VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700EV,SHPW,,1700 END VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700SCR,SCREENS,,SCREEN FOR #1700 SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,1700VS,SHPW,,#1700 VENT SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2000SCR,SHPT,,C-2000 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,2100EV,2000SLI,#N/A,SERIES 2100 3-PANEL SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,2200PDG,FPPW,,SERIES 2200 TB FIXED LITE W/TEMP GLASS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,2650SCR,SCREENS,,SCREENS FOR 2650,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2650VP,SHPW,,#2650 VENT PANEL - SINGLE HUNGE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2700CV,SHPW,,C-2700 - CENTER VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2700EV,SHPW,,C-2700 - END VENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2700SCR,SCREENS,,SCREENS FOR #2700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,2700VS,SHPW,,#2700 VENT SASH,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,3000SCR,SCREENS,,SCREENS FOR #3000,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,305INS,INSERTS,,#305 SASH WINDCHECK INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,306INS,INSERTS,,#306 SCHLEGEL GLASS INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,3100EV,3000DHP,#N/A,SERIES 3100 3-PANEL SLIDER,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3100SCR,SCREENS,,SCREENS FOR #3100,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,310PSCR,SCRINS,,#310 PLAIN SCREENS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,3700SCR,SCREENS,,SCREEN FOR #3700 POLE BARN WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,3700VP,SHPW,,#3700 VENT PANEL,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,404ONE,STPW,,#404 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,450ONE,STPW,,#450 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,4700SCR,SCREENS,,SCREENS FOR #4700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,5000SCR,SCREENS,,SCREEN FOR R5000 SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,606ONE,STPW,,#606 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,6100I,STD,,STAR 6100 FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,6100L,STD,,STAR 6100 FULL VIEW STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,650ONE,STPW,,#650 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
TRUE,Iola,7100I,STD,,STAR 7100 FULL VIEW SELF STORING DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,7100L,STD,,STAR 7100 FULL VIEW SELF STORING DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,808ONE,STPW,,#808 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,8100I,STD,,STAR 8100 FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,8100L,STD,,STAR 8100 FULL VIEW STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,850ONE,STPW,,#850 ONE-LITE ST WD,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,BELMONT,3000FPP,#N/A,BELMONT ALLIANCE DOUBLE HUNG WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
FALSE,Iola,BGI,INSERTS,,BOTTOM GL INSERTS FOR ECONOMY ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,BGI404,INSERTS,,BOTTOM GL INSERTS FOR #404/450 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,BGI606,INSERTS,,BOTTOM GL INSERTS FOR #606/650 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,BGIST,INSERTS,,INSERTS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,C-500,SHPW,,C-500 SH THERMAL BREAK INS.,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,C1150,CASEMNT,,C1150-VINYL AWNING WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C1500AR,CIR TOP,,C-1510 VINYL ARCH TOP-OPERATING WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C1510AR,CIR TOP,,C-1510 VINYL ARCH TOP WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C1521,CIR TOP,,C-1521 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C1526,CIR TOP,,C-1526 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1621,CIR TOP,,C-1621 INSULATED CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1622,CIR TOP,,C-1622 INSULATED CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1626VA,CIR TOP,,C-1626VA INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1710,SHPW,,#1710 PICTURE OVER SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,C1721,CIR TOP,,C-1721 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1722,CIR TOP,,C-1722 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1724,CIR TOP,,C-1724 INSULATED CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C1800,C1800,#N/A,C-1800 INSIDE SLIDING STORM WINDOW,Window,,Storm Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C2021,CIR TOP,,C-2021 T.B CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C2022,CIR TOP,,C-2022 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C2026,CIR TOP,,C-2026 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C2710,SHPW,,#2710 PICTURE OVER SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,C300,BSMT,#N/A,C-300 ALUM INSERTS FOR BASEMENT BUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C3221,CIR TOP,,C-3221 T.B CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C3222,CIR TOP,,C-3222 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C3223,CIR TOP,,C-3223 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C3224,CIR TOP,,C-3224 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C3300,CASEMNT,,3300 1-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C3700,SHPW,,C-3700 INSULATED SLIDING POLE BARN WND,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C400,BSMT,#N/A,C-400 VINYL INSERT FOR BASEMENT BUCKS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C4000,REPLACE,,C-4000 SINGLE HUNGE PRIMARY WINDOW,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C4260,PD,,C-4260 STEEL MIRROR DOOR,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C500,SHPW,,C-500 INS THERMAL BREAK SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,C500GL,VENTS,,LITES OF INSULATED GLASS FOR C-500'S,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C500GLP,VENTS,,LITES OF INSULATED GLASS FOR C-500'S,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C500PP,SHPW,,C-500 INS THERMAL BREAK SINGLE HUNG,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,C500SCR,SCREENS,,C-500 SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,C500VP,VENTS,,VENT PANELS FOR C-500,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,C521,CIR TOP,,C-521 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C522,CIR TOP,,C-522 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C526,CIR TOP,,C-526 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
TRUE,Iola,C610,CASEMNT,,C610 VINYL CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C620,FPPW,,C620 VINYL AWNING WINDOWS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C621,CIR TOP,,C-621 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C626,CIR TOP,,C-626 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C640,FPPW,,C-640 VINYL FIXED CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C826,CIR TOP,,C-826 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C828,CIR TOP,,C-828 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C8321,CIR TOP,,C-8321 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,C8326,CIR TOP,,C-8326 VINYL CIRCLE TOP,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C900,SHPW,,C-900 INS THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C900EV,SHPW,,C-900 ENDS VENT SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C900SCR,SCREENS,,SCREEN FOR C-900,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C910,FPPW,,C-910 INSULATED T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C910PP,FPPW,,C-910 ARCH TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,C921,CIR TOP,,C-921 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C922,CIR TOP,,C-922 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C924,CIR TOP,,C-924 T.B. CIRCLE TOPS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C931,CIR TOP,,C-931 T.B. ROUND PRIMARY WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C939,CIR TOP,,C-939 T.B. ROUND PRIMARY WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C940,CIR TOP,,C-940 T.B. OCTAGON PRIMARY,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C949,CIR TOP,,C949 OCTAGON THERMAL BREAK WINDOW,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,C960,FPPW,,C-960 INSULATED T.B. CIRCLE TOP,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,COBRAI,STD,,COLUMBIA COBRA STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,COBRAL,STD,,COLUMBIA COBRA STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,COBRATI,STD,,COLUMBIA COBRA STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,COBRATL,STD,,COLUMBIA COBRA STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,CRWNFVI,STD,,CROWN FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,CRWNFVL,STD,,CROWN FULL VIEW STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,CRWNSDI,STD,,COLUMBIA CROWN SCREEN DOOR,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,CRWNSDL,STD,,COLUMBIA CROWN SCREEN DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,D770,SHPT,,D770 D.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,D780,SHPW,,D780 DOUBLE SLIDE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,D830,SHPT,,D830 D.H.TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,D830SCR,SHPT,,D830 SCREEN,Window,,Primary Window,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,D832,FPPW,,D832 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,DSGLASS,GLASS,,DOUBLE STRENGTH GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,DURASEA,FPPW,,"DURASEAL 5/8"" (GRAY) PER REEL",,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,EXPAND,EXPANDR,,SILL EXPANDERS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,FULLSCR,SCREENS,,FULL SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,FV10I,STD,,KING FV-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,FV10L,STD,,KING FV-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,FV3I,STD,,KING FV-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,FV3L,STD,,KING FV-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,FVGI,INSERTS,,GLASS INSERTS FOR KING ONE LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,FVSI,INSERTS,,FULL SCREENS ONLY FOR KING ONE-LITES,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,G3000,GARDEN,,COLUMBIA COMFORT 3000 VINYL GARDEN WD,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,GOLIATH,STD,,GOLIATH STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,HERCULE,STD,,HERCULES STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,IMPERIL,PD,,IMPERIAL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,INSGLAS,GLASS,,INSULATED GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,ISP,PD,,STATIONARY PANELS FOR IMPERIAL DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,IVP,PD,,VENT PANELS FOR IMPERIAL PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,JET,PD,,COLUMBIA JET PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,JSP,PD,,STATIONARY PANELS FOR JET DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,JVP,PD,,VENT PANELS FOR JET PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,KINGDVI,STD,,KING DUAL VENT STORM DOORS,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,KINGDVL,STD,,KING DUAL VENT STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,KINGFSC,INSERTS,,FULL SCREEN FOR KING ONE-LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,KINGI,STD,,KING ONE-LITE STORM DOORS,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,KINGL,STD,,KING ONE-LITE STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,KINGSDI,STD,,KING ONE-LITE SCREEN DOOR ONLY,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,KINGSDL,STD,,KING ONE-LITE SCREEN DOOR ONLY,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,LINCOLN,3000DHP,#N/A,LINCOLN PRIMARY WOOD WINDOWS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,LINCPDR,PD,,LINCOLN FRENCH PATIO DOOR,Door,Patio Door,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,M1200,PD,,M1200 PATIO STORM DOOR,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,M306,INSERTS,,M-306 SCHLEGEL GLASS INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,OUTSIDE,STDMISC,,OUTSIDE DOOR SWEEPS - ALUM + VINYL,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,PATIOSC,SCREENS,,PATIO DOOR SCREENS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,PDSCRTT,SCREENS,,SPECIAL SIZE SCREENS FOR PATIO DOORS,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,PRPDS,SCREENS,,SCREENS MADE FROM PLAIN PATIO SCR RAIL,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,PRSCR,PSCREEN,,SCREENS FOR PRIME MADE FROM #19-88,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,
FALSE,Iola,PRSCR11,PSCREEN,,SCREEN FOR PRIME MADE FROM #19-11,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,PSINS,INSERTS,,PLAIN SASH INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,PWS,STPW,,PIN-ON PICTURE WINDOWS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE,
FALSE,Iola,PWSINS,INSERTS,,INSERTS ONLY FOR PIN-ON PICTURE WINDOW,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,R1150,REPLACE,,R-1150 VINYL AWNING WINDOWS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R1400,REPLACE,,SERIES 1400 VINYL SLIDING PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R1500,REPLACE,,SERIES 1500 S.H. TILT VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R1510,FPPW,,SERIES 1510 FIXED LITE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R2000,REPLACE,,SERIES 2000 S.H. T.B. TILT PRIMARY,Window,,Primary Window,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,R2100,REPLACE,,SERIES 2100 THERMAL BREAK SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,R2100EV,R2000SL,,SERIES 2100 3-PANEL T.B. SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,R2200,RFPPW,,SERIES 2200 T.B. FIXED LITE,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,R300,RBSMT,,C-300 ALUM INSERTS FOR BASEMENT BUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R3302,CASEMNT,,3302 2-PANEL T.B. CASEMENT,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,R400,RBSMT,,C-400 VINYL INSERT FOR BASEMENT BUCKS,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R5000,REPLACE,,R-5000 INSULATED ALUMINUM SLIDER,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R770,REPLACE,,R770 D.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,R770SCR,SCREENS,,FULL SCREEN,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R780,REPLACE,,R780 VINYL SLIDING PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R820,REPLACE,,R820 S.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R821,REPLACE,,S821 SINGLE SLIDE VINYL PRIMARY,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R822,FPPW,,S822 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R830,REPLACE,,R830 D.H.TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,R832,FPPW,,R832 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,RCKTRAP,INSERTS,,ROCKET TRAPEZOIDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,REWIRE,SCREENS,,REWIRED SCREENS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,RNDROCK,INSERTS,,ROUND ROCKET INSERT,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,ROCKET,INSERTS,,ROCKET INSERTS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
TRUE,Iola,ROYAL,STD,,COLUMBIA ROYAL STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,RROCKET,INSERTS,,RADIUS ROCKETS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
TRUE,Iola,S820,SHPT,,S820 S.H. TILT VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,S821,SHPW,,S821 SINGLE SLIDE VINYL PRIMARY,,,,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,S822,FPPW,,S822 FIXED VINYL PRIMARY WINDOWS,Window,,Primary Window,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,SCRI,INSERTS,,SCREEN INSERTS FOR ECONOMY STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,SCRI404,INSERTS,,SCREEN INSERTS FOR #404/450 STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,SCRI606,INSERTS,,SCREEN INSERTS FOR #606/650 STORM WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,SCRI808,INSERTS,,SCREEN INSERTS FOR #808/850 STORM WIND,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
TRUE,Iola,SS10I,STD,,COBRA SS-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,SS10L,STD,,COBRA SS-10 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,SS3I,STD,,COBRA SS-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,SS3L,STD,,COBRA SS-3 DECORATOR STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,SSGLASS,GLASS,,SINGLE STRENGTH GLASS,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,SSSCR,INSERTS,,SCREEN INSERT FOR SELF STORING DOORS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,TBGI,INSERTS,,TEMPERED GLASS INSERTS FOR SS DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,TBR,PD,,IMPERIAL T.B.R. REPLACEMENT PATIO DOOR,Door,Patio Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,TGI,INSERTS,,TOP GL INSERTS FOR ECONOMY ST WDS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,TRUE,FALSE,
FALSE,Iola,TGI404,INSERTS,,TOP GLASS INSERTS FOR #404/450 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,TGI606,INSERTS,,TOP GLASS INSERTS FOR #606/650 ST WDS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,
FALSE,Iola,TGIODD,INSERTS,,TOP GLASS INSERTS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,THOR,STD,,THOR - STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,TIARAI,STD,,COLUMBIA TIARA SELF STORING STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,TIARAL,STD,,COLUMBIA TIARA SELF STORING STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
TRUE,Iola,TVGROOV,INSERTS,,TEMPERED V-GROOVE ONE-LITE INSERTS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,TVI,STD,,COLUMBIA COBRA TWIN VENT STORM DOOR,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,TVL,STD,,COLUMBIA COBRA TWIN VENT STORM DOOR,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,VKI,STOCK,,VENTILATOR KICKPANEL FOR KING ONELITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
TRUE,Iola,VKS,STOCK,,VENTILATOR SCREEN FOR KING ONE LITE,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,Iola,VP3700,VENTS,,VENT PANELS FOR #3700,,,,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE,FALSE,FALSE,FALSE,
FALSE,Iola,WINDGAT,VACW,,ALLIANCE WINDGATE CASEMENT WINDOW,,,,FALSE,FALSE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,
TRUE,Iola,XBUCKIN,INSERTS,,TEMPERED GLASS INSERTS FOR CROSSBUCKS,,,,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,Iola,ZBARS,STDMISC,,Z-BARS FOR STORM DOORS,,,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,KC,COBRATK,STD,,Cobra aluminum storm door with Glass Kick Panel,Door,Storm Door,,FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE,FALSE,FALSE,TRUE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
FALSE,~,,,,,,,,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,
1 Base Sub-type Accessory Materials Colors
2 DISCONT LOC_CODE PROD_CODE CATEGORY CATEGORY_NEW DESCRIPTION Type Door Window Yes This Item Aluminum Vinyl Black White Bronze Tan Mill Sandstone ~
3 FALSE Iola 1650-10 FPPW #1650-10 INSULATED FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
4 TRUE Iola 303 STORMS #303 DART STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
5 TRUE Iola 350 STORMS #350 ARROW STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
6 FALSE Iola 404 STORMS #404 FALCON STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
7 FALSE Iola 450 STORMS #450 RAVEN STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
8 FALSE Iola 606 STORMS #606 LION STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
9 FALSE Iola 650 STORMS #650 LYNX STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
10 TRUE Iola 808 STORMS 808 HAWK STORM WINDOW Window Storm Window FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
11 TRUE Iola 850 STORMS #850 KENT STORM WINDOWS Window Storm Window FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
12 FALSE Iola 1400 SHPW SERIES 1400 VINYL SLIDING PRIMARY FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
13 FALSE Iola 1500 SHPT SERIES 1500 S.H. TILT VINYL PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
14 FALSE Iola 1510 FPPW 1510 VINYL INSULATED FIXED LITE FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
15 FALSE Iola 1650 SHPW #1650 INSULATED SINGLE HUNG FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
16 FALSE Iola 1700 SHPW #1700 INSULATED SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
17 FALSE Iola 1710 FPPW C-1710 FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
18 FALSE Iola 2000 SHPT SERIES 2000 S.H. T.B. TILT PRIMARY Window Primary Window FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
19 FALSE Iola 2100 SHPT SERIES 2100 THERMAL BREAK SLIDER FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
20 FALSE Iola 2200 FPPW SERIES 2200 T.B. FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
21 TRUE Iola 2400 PD 2400 ROYAL CROWN PATIO DOOR Door Patio Door FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
22 FALSE Iola 2650 SHPW #2650 SINGLE HUNG SINGLE GLAZED PRIME FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
23 FALSE Iola 2700 SHPW #2700 SINGLE GLAZED SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
24 FALSE Iola 2710 FPPW C-2710 FIXED LITE - SINGLE GLAZED FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
25 FALSE Iola 3000 3000DHP #N/A SERIES 3000 D.H. T.B REPLACEMENT WD FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
26 FALSE Iola 3100 3000DHP #N/A SERIES 3100 T.B. REPLACEMENT SLIDER FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
27 FALSE Iola 3200 3000FPP #N/A SERIES 3200 T.B. PICTURE WINDOW FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
28 FALSE Iola 3300 CASEMNT 3300 1-PANEL T.B. CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
29 FALSE Iola 3302 CASEMNT 3300 2-PANEL T.B. CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
30 FALSE Iola 3303 CASEMNT 3300 3-PANEL T.B. CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
31 FALSE Iola 3310 CASEMNT 3310 FIXED CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
32 TRUE Iola 3400 CASEMNT 3400 1 PANEL VINYL CLAD CASEMENT FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
33 TRUE Iola 3402 CASEMNT 3400 2 PANEL VINYL CLAD CASEMENT FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
34 TRUE Iola 3403 CASEMNT 3400 3 PANEL VINYL CLAD CASEMENT FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
35 TRUE Iola 3500 CASEMNT 3500 1-PANEL WOOD INTERIOR CASEMENT FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
36 TRUE Iola 3502 CASEMNT 3500 2-PANEL WOOD INTERIOR CASEMENT FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
37 TRUE Iola 3503 CASEMNT 3503 3-PANEL WOOD INTERIOR CASEMENT FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
38 FALSE Iola 3700 SHPW #3700 INSULATED SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
39 FALSE Iola 3710 FPPW C-3710 INSULATED FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
40 TRUE Iola 4100 REPLACE SERIES 4100 VINYL SLIDER FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
41 FALSE Iola 4700 SHPW #4700 SINGLE GLAZED SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
42 FALSE Iola 4710 FPPW #4710 SINGLE GLAZED FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
43 FALSE Iola 5200 PD 5200 VINYL PATIO DOORS Door Patio Door FALSE FALSE FALSE TRUE FALSE TRUE FALSE TRUE FALSE FALSE
44 FALSE Iola 5700 SHPW #5700 INSULATED SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
45 TRUE Iola 6300 SHPT SERIES 6300 S.H. TILT VINYL PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
46 TRUE Iola 6301 SHPW SERIES 6301 VINYL SLIDING PRIMARY FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
47 FALSE Iola 6700 SHPW #6700 SINGLE GLAZED SLIDER FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
48 FALSE Iola 265010 FPPW #2650-10 SINGLE GLAZED FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
49 FALSE Iola 1400SCR SHPW 1400 SCREEN FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE
50 FALSE Iola 1650SCR SCREENS SCREENS FOR #1650 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
51 FALSE Iola 1650VS SHPW 1650 BOTTOM SASH FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
52 FALSE Iola 1700CSC SCREENS SCREEN FOR #1700 CENTER VENT SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
53 FALSE Iola 1700CV SHPW 1700 CENTER VENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
54 FALSE Iola 1700ESC SCREENS SCREEN FOR #1700 ENDS VENT SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
55 FALSE Iola 1700EV SHPW 1700 END VENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
56 FALSE Iola 1700SCR SCREENS SCREEN FOR #1700 SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
57 FALSE Iola 1700VS SHPW #1700 VENT SASH FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
58 FALSE Iola 2000SCR SHPT C-2000 SCREEN FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
59 FALSE Iola 2100EV 2000SLI #N/A SERIES 2100 3-PANEL SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
60 FALSE Iola 2200PDG FPPW SERIES 2200 TB FIXED LITE W/TEMP GLASS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
61 FALSE Iola 2650SCR SCREENS SCREENS FOR 2650 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
62 FALSE Iola 2650VP SHPW #2650 VENT PANEL - SINGLE HUNGE FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
63 FALSE Iola 2700CV SHPW C-2700 - CENTER VENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
64 FALSE Iola 2700EV SHPW C-2700 - END VENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
65 FALSE Iola 2700SCR SCREENS SCREENS FOR #2700 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
66 FALSE Iola 2700VS SHPW #2700 VENT SASH FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
67 FALSE Iola 3000SCR SCREENS SCREENS FOR #3000 FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
68 FALSE Iola 305INS INSERTS #305 SASH WINDCHECK INSERTS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
69 FALSE Iola 306INS INSERTS #306 SCHLEGEL GLASS INSERTS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
70 FALSE Iola 3100EV 3000DHP #N/A SERIES 3100 3-PANEL SLIDER FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
71 FALSE Iola 3100SCR SCREENS SCREENS FOR #3100 FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
72 FALSE Iola 310PSCR SCRINS #310 PLAIN SCREENS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
73 FALSE Iola 3700SCR SCREENS SCREEN FOR #3700 POLE BARN WD FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
74 FALSE Iola 3700VP SHPW #3700 VENT PANEL FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
75 FALSE Iola 404ONE STPW #404 ONE-LITE ST WD FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
76 FALSE Iola 450ONE STPW #450 ONE-LITE ST WD FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
77 FALSE Iola 4700SCR SCREENS SCREENS FOR #4700 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
78 TRUE Iola 5000SCR SCREENS SCREEN FOR R5000 SLIDER FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
79 FALSE Iola 606ONE STPW #606 ONE-LITE ST WD FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE
80 FALSE Iola 6100I STD STAR 6100 FULL VIEW STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
81 TRUE Iola 6100L STD STAR 6100 FULL VIEW STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
82 FALSE Iola 650ONE STPW #650 ONE-LITE ST WD FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
83 TRUE Iola 7100I STD STAR 7100 FULL VIEW SELF STORING DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
84 TRUE Iola 7100L STD STAR 7100 FULL VIEW SELF STORING DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
85 TRUE Iola 808ONE STPW #808 ONE-LITE ST WD FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
86 FALSE Iola 8100I STD STAR 8100 FULL VIEW STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
87 TRUE Iola 8100L STD STAR 8100 FULL VIEW STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
88 TRUE Iola 850ONE STPW #850 ONE-LITE ST WD FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
89 FALSE Iola BELMONT 3000FPP #N/A BELMONT ALLIANCE DOUBLE HUNG WINDOW FALSE FALSE FALSE TRUE FALSE TRUE FALSE TRUE FALSE FALSE
90 FALSE Iola BGI INSERTS BOTTOM GL INSERTS FOR ECONOMY ST WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
91 FALSE Iola BGI404 INSERTS BOTTOM GL INSERTS FOR #404/450 ST WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
92 FALSE Iola BGI606 INSERTS BOTTOM GL INSERTS FOR #606/650 ST WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
93 FALSE Iola BGIST INSERTS INSERTS FOR STORM DOORS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
94 FALSE Iola C-500 SHPW C-500 SH THERMAL BREAK INS. FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE
95 FALSE Iola C1150 CASEMNT C1150-VINYL AWNING WINDOW FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
96 TRUE Iola C1500AR CIR TOP C-1510 VINYL ARCH TOP-OPERATING WINDOW FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
97 TRUE Iola C1510AR CIR TOP C-1510 VINYL ARCH TOP WINDOW FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
98 TRUE Iola C1521 CIR TOP C-1521 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
99 TRUE Iola C1526 CIR TOP C-1526 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
100 FALSE Iola C1621 CIR TOP C-1621 INSULATED CIRCLE TOP FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
101 FALSE Iola C1622 CIR TOP C-1622 INSULATED CIRCLE TOP FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
102 FALSE Iola C1626VA CIR TOP C-1626VA INSULATED CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
103 FALSE Iola C1710 SHPW #1710 PICTURE OVER SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
104 FALSE Iola C1721 CIR TOP C-1721 INSULATED CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
105 FALSE Iola C1722 CIR TOP C-1722 INSULATED CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
106 FALSE Iola C1724 CIR TOP C-1724 INSULATED CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
107 FALSE Iola C1800 C1800 #N/A C-1800 INSIDE SLIDING STORM WINDOW Window Storm Window FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
108 FALSE Iola C2021 CIR TOP C-2021 T.B CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
109 FALSE Iola C2022 CIR TOP C-2022 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
110 FALSE Iola C2026 CIR TOP C-2026 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
111 FALSE Iola C2710 SHPW #2710 PICTURE OVER SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
112 FALSE Iola C300 BSMT #N/A C-300 ALUM INSERTS FOR BASEMENT BUCKS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
113 FALSE Iola C3221 CIR TOP C-3221 T.B CIRCLE TOPS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
114 FALSE Iola C3222 CIR TOP C-3222 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
115 FALSE Iola C3223 CIR TOP C-3223 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
116 FALSE Iola C3224 CIR TOP C-3224 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
117 FALSE Iola C3300 CASEMNT 3300 1-PANEL T.B. CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
118 FALSE Iola C3700 SHPW C-3700 INSULATED SLIDING POLE BARN WND FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
119 FALSE Iola C400 BSMT #N/A C-400 VINYL INSERT FOR BASEMENT BUCKS FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
120 TRUE Iola C4000 REPLACE C-4000 SINGLE HUNGE PRIMARY WINDOW Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
121 TRUE Iola C4260 PD C-4260 STEEL MIRROR DOOR FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
122 FALSE Iola C500 SHPW C-500 INS THERMAL BREAK SINGLE HUNG FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE
123 FALSE Iola C500GL VENTS LITES OF INSULATED GLASS FOR C-500'S FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
124 FALSE Iola C500GLP VENTS LITES OF INSULATED GLASS FOR C-500'S FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
125 FALSE Iola C500PP SHPW C-500 INS THERMAL BREAK SINGLE HUNG FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE
126 FALSE Iola C500SCR SCREENS C-500 SCREEN FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE
127 FALSE Iola C500VP VENTS VENT PANELS FOR C-500 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE TRUE
128 FALSE Iola C521 CIR TOP C-521 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
129 FALSE Iola C522 CIR TOP C-522 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
130 FALSE Iola C526 CIR TOP C-526 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
131 TRUE Iola C610 CASEMNT C610 VINYL CASEMENT WINDOW FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
132 TRUE Iola C620 FPPW C620 VINYL AWNING WINDOWS FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
133 TRUE Iola C621 CIR TOP C-621 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
134 TRUE Iola C626 CIR TOP C-626 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
135 TRUE Iola C640 FPPW C-640 VINYL FIXED CASEMENT WINDOW FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
136 TRUE Iola C826 CIR TOP C-826 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
137 TRUE Iola C828 CIR TOP C-828 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
138 TRUE Iola C8321 CIR TOP C-8321 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
139 TRUE Iola C8326 CIR TOP C-8326 VINYL CIRCLE TOP FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
140 FALSE Iola C900 SHPW C-900 INS THERMAL BREAK SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
141 FALSE Iola C900EV SHPW C-900 ENDS VENT SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
142 FALSE Iola C900SCR SCREENS SCREEN FOR C-900 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
143 FALSE Iola C910 FPPW C-910 INSULATED T.B. FIXED LITE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
144 FALSE Iola C910PP FPPW C-910 ARCH TOP FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
145 FALSE Iola C921 CIR TOP C-921 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
146 FALSE Iola C922 CIR TOP C-922 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
147 FALSE Iola C924 CIR TOP C-924 T.B. CIRCLE TOPS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
148 FALSE Iola C931 CIR TOP C-931 T.B. ROUND PRIMARY WINDOW FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
149 FALSE Iola C939 CIR TOP C-939 T.B. ROUND PRIMARY WINDOW FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
150 FALSE Iola C940 CIR TOP C-940 T.B. OCTAGON PRIMARY FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
151 FALSE Iola C949 CIR TOP C949 OCTAGON THERMAL BREAK WINDOW FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
152 FALSE Iola C960 FPPW C-960 INSULATED T.B. CIRCLE TOP FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
153 FALSE Iola COBRAI STD COLUMBIA COBRA STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
154 TRUE Iola COBRAL STD COLUMBIA COBRA STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
155 FALSE Iola COBRATI STD COLUMBIA COBRA STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
156 TRUE Iola COBRATL STD COLUMBIA COBRA STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
157 FALSE Iola CRWNFVI STD CROWN FULL VIEW STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
158 TRUE Iola CRWNFVL STD CROWN FULL VIEW STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
159 FALSE Iola CRWNSDI STD COLUMBIA CROWN SCREEN DOOR FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
160 TRUE Iola CRWNSDL STD COLUMBIA CROWN SCREEN DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
161 FALSE Iola D770 SHPT D770 D.H. TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
162 TRUE Iola D780 SHPW D780 DOUBLE SLIDE VINYL PRIMARY FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
163 TRUE Iola D830 SHPT D830 D.H.TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
164 TRUE Iola D830SCR SHPT D830 SCREEN Window Primary Window FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
165 TRUE Iola D832 FPPW D832 FIXED VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
166 FALSE Iola DSGLASS GLASS DOUBLE STRENGTH GLASS FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
167 FALSE Iola DURASEA FPPW DURASEAL 5/8" (GRAY) PER REEL FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
168 FALSE Iola EXPAND EXPANDR SILL EXPANDERS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
169 FALSE Iola FULLSCR SCREENS FULL SCREEN FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
170 TRUE Iola FV10I STD KING FV-10 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
171 TRUE Iola FV10L STD KING FV-10 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
172 TRUE Iola FV3I STD KING FV-3 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
173 TRUE Iola FV3L STD KING FV-3 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
174 FALSE Iola FVGI INSERTS GLASS INSERTS FOR KING ONE LITE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
175 FALSE Iola FVSI INSERTS FULL SCREENS ONLY FOR KING ONE-LITES FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
176 TRUE Iola G3000 GARDEN COLUMBIA COMFORT 3000 VINYL GARDEN WD FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
177 FALSE Iola GOLIATH STD GOLIATH STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
178 FALSE Iola HERCULE STD HERCULES STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
179 FALSE Iola IMPERIL PD IMPERIAL PATIO DOORS Door Patio Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
180 FALSE Iola INSGLAS GLASS INSULATED GLASS FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
181 FALSE Iola ISP PD STATIONARY PANELS FOR IMPERIAL DOORS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
182 FALSE Iola IVP PD VENT PANELS FOR IMPERIAL PATIO DOORS Door Patio Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
183 FALSE Iola JET PD COLUMBIA JET PATIO DOORS Door Patio Door FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
184 FALSE Iola JSP PD STATIONARY PANELS FOR JET DOORS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
185 FALSE Iola JVP PD VENT PANELS FOR JET PATIO DOORS Door Patio Door FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
186 FALSE Iola KINGDVI STD KING DUAL VENT STORM DOORS Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
187 TRUE Iola KINGDVL STD KING DUAL VENT STORM DOORS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
188 FALSE Iola KINGFSC INSERTS FULL SCREEN FOR KING ONE-LITE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
189 FALSE Iola KINGI STD KING ONE-LITE STORM DOORS Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
190 TRUE Iola KINGL STD KING ONE-LITE STORM DOORS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
191 FALSE Iola KINGSDI STD KING ONE-LITE SCREEN DOOR ONLY Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
192 TRUE Iola KINGSDL STD KING ONE-LITE SCREEN DOOR ONLY FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
193 TRUE Iola LINCOLN 3000DHP #N/A LINCOLN PRIMARY WOOD WINDOWS FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
194 TRUE Iola LINCPDR PD LINCOLN FRENCH PATIO DOOR Door Patio Door FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
195 FALSE Iola M1200 PD M1200 PATIO STORM DOOR Door Patio Door FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
196 FALSE Iola M306 INSERTS M-306 SCHLEGEL GLASS INSERTS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
197 FALSE Iola OUTSIDE STDMISC OUTSIDE DOOR SWEEPS - ALUM + VINYL FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
198 FALSE Iola PATIOSC SCREENS PATIO DOOR SCREENS Door Patio Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
199 FALSE Iola PDSCRTT SCREENS SPECIAL SIZE SCREENS FOR PATIO DOORS Door Patio Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
200 FALSE Iola PRPDS SCREENS SCREENS MADE FROM PLAIN PATIO SCR RAIL FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
201 FALSE Iola PRSCR PSCREEN SCREENS FOR PRIME MADE FROM #19-88 FALSE FALSE TRUE FALSE FALSE TRUE TRUE TRUE TRUE TRUE
202 FALSE Iola PRSCR11 PSCREEN SCREEN FOR PRIME MADE FROM #19-11 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
203 FALSE Iola PSINS INSERTS PLAIN SASH INSERTS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
204 FALSE Iola PWS STPW PIN-ON PICTURE WINDOWS FALSE FALSE TRUE FALSE TRUE TRUE TRUE TRUE TRUE TRUE
205 FALSE Iola PWSINS INSERTS INSERTS ONLY FOR PIN-ON PICTURE WINDOW FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
206 FALSE Iola R1150 REPLACE R-1150 VINYL AWNING WINDOWS FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
207 FALSE Iola R1400 REPLACE SERIES 1400 VINYL SLIDING PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
208 FALSE Iola R1500 REPLACE SERIES 1500 S.H. TILT VINYL PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
209 FALSE Iola R1510 FPPW SERIES 1510 FIXED LITE VINYL PRIMARY FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
210 FALSE Iola R2000 REPLACE SERIES 2000 S.H. T.B. TILT PRIMARY Window Primary Window FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
211 FALSE Iola R2100 REPLACE SERIES 2100 THERMAL BREAK SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
212 FALSE Iola R2100EV R2000SL SERIES 2100 3-PANEL T.B. SLIDER FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
213 FALSE Iola R2200 RFPPW SERIES 2200 T.B. FIXED LITE FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
214 FALSE Iola R300 RBSMT C-300 ALUM INSERTS FOR BASEMENT BUCKS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
215 FALSE Iola R3302 CASEMNT 3302 2-PANEL T.B. CASEMENT FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
216 FALSE Iola R400 RBSMT C-400 VINYL INSERT FOR BASEMENT BUCKS FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
217 TRUE Iola R5000 REPLACE R-5000 INSULATED ALUMINUM SLIDER FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
218 FALSE Iola R770 REPLACE R770 D.H. TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
219 FALSE Iola R770SCR SCREENS FULL SCREEN FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
220 TRUE Iola R780 REPLACE R780 VINYL SLIDING PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
221 TRUE Iola R820 REPLACE R820 S.H. TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
222 TRUE Iola R821 REPLACE S821 SINGLE SLIDE VINYL PRIMARY Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
223 TRUE Iola R822 FPPW S822 FIXED VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
224 TRUE Iola R830 REPLACE R830 D.H.TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
225 TRUE Iola R832 FPPW R832 FIXED VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
226 FALSE Iola RCKTRAP INSERTS ROCKET TRAPEZOIDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
227 FALSE Iola REWIRE SCREENS REWIRED SCREENS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
228 FALSE Iola RNDROCK INSERTS ROUND ROCKET INSERT FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
229 FALSE Iola ROCKET INSERTS ROCKET INSERTS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
230 TRUE Iola ROYAL STD COLUMBIA ROYAL STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
231 FALSE Iola RROCKET INSERTS RADIUS ROCKETS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
232 TRUE Iola S820 SHPT S820 S.H. TILT VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
233 TRUE Iola S821 SHPW S821 SINGLE SLIDE VINYL PRIMARY FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
234 TRUE Iola S822 FPPW S822 FIXED VINYL PRIMARY WINDOWS Window Primary Window FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
235 FALSE Iola SCRI INSERTS SCREEN INSERTS FOR ECONOMY STORM WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
236 FALSE Iola SCRI404 INSERTS SCREEN INSERTS FOR #404/450 STORM WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
237 FALSE Iola SCRI606 INSERTS SCREEN INSERTS FOR #606/650 STORM WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
238 FALSE Iola SCRI808 INSERTS SCREEN INSERTS FOR #808/850 STORM WIND FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
239 TRUE Iola SS10I STD COBRA SS-10 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
240 TRUE Iola SS10L STD COBRA SS-10 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
241 TRUE Iola SS3I STD COBRA SS-3 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
242 TRUE Iola SS3L STD COBRA SS-3 DECORATOR STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
243 FALSE Iola SSGLASS GLASS SINGLE STRENGTH GLASS FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
244 FALSE Iola SSSCR INSERTS SCREEN INSERT FOR SELF STORING DOORS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
245 FALSE Iola TBGI INSERTS TEMPERED GLASS INSERTS FOR SS DOORS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
246 FALSE Iola TBR PD IMPERIAL T.B.R. REPLACEMENT PATIO DOOR Door Patio Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE
247 FALSE Iola TGI INSERTS TOP GL INSERTS FOR ECONOMY ST WDS FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE TRUE FALSE
248 FALSE Iola TGI404 INSERTS TOP GLASS INSERTS FOR #404/450 ST WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
249 FALSE Iola TGI606 INSERTS TOP GLASS INSERTS FOR #606/650 ST WDS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE TRUE
250 FALSE Iola TGIODD INSERTS TOP GLASS INSERTS FOR STORM DOORS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
251 FALSE Iola THOR STD THOR - STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
252 TRUE Iola TIARAI STD COLUMBIA TIARA SELF STORING STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
253 TRUE Iola TIARAL STD COLUMBIA TIARA SELF STORING STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
254 TRUE Iola TVGROOV INSERTS TEMPERED V-GROOVE ONE-LITE INSERTS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
255 FALSE Iola TVI STD COLUMBIA COBRA TWIN VENT STORM DOOR Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
256 TRUE Iola TVL STD COLUMBIA COBRA TWIN VENT STORM DOOR FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
257 FALSE Iola VKI STOCK VENTILATOR KICKPANEL FOR KING ONELITE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
258 TRUE Iola VKS STOCK VENTILATOR SCREEN FOR KING ONE LITE FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
259 FALSE Iola VP3700 VENTS VENT PANELS FOR #3700 FALSE FALSE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE
260 FALSE Iola WINDGAT VACW ALLIANCE WINDGATE CASEMENT WINDOW FALSE FALSE FALSE TRUE FALSE TRUE FALSE TRUE FALSE FALSE
261 TRUE Iola XBUCKIN INSERTS TEMPERED GLASS INSERTS FOR CROSSBUCKS FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
262 FALSE Iola ZBARS STDMISC Z-BARS FOR STORM DOORS FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
263 FALSE KC COBRATK STD Cobra aluminum storm door with Glass Kick Panel Door Storm Door FALSE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE
264 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
265 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
266 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
267 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
268 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
269 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
270 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
271 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
272 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
273 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
274 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
275 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
276 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
277 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
278 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
279 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
280 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
281 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
282 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
283 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
284 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
285 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
286 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
287 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
288 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
289 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
290 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
291 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
292 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
293 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
294 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
295 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
296 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
297 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
298 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
299 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
300 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
301 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
302 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
303 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
304 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
305 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
306 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
307 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
308 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
309 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
310 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
311 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
312 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
313 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
314 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
315 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
316 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
317 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
318 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
319 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
320 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
321 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
322 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
323 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
324 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
325 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
326 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
327 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
328 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
329 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
330 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
331 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
332 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
333 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
334 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
335 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
336 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
337 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
338 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
339 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
340 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
341 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
342 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
343 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
344 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
345 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
346 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
347 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
348 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
349 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
350 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
351 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
352 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
353 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
354 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
355 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
356 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
357 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
358 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
359 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
360 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
361 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
362 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
363 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
364 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
365 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
366 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
367 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
368 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
369 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
370 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
371 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
372 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
373 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
374 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
375 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
376 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
377 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
378 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
379 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
380 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
381 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
382 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
383 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
384 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
385 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
386 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
387 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
388 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
389 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
390 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
391 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
392 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
393 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
394 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
395 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
396 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
397 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
398 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
399 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
400 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
401 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
402 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
403 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
404 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
405 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
406 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
407 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
408 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
409 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
410 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
411 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
412 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
413 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
414 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
415 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
416 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
417 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
418 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
419 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
420 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
421 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
422 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
423 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
424 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
425 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
426 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
427 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
428 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
429 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
430 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
431 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
432 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
433 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
434 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
435 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
436 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
437 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
438 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
439 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
440 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
441 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
442 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
443 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
444 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
445 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
446 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
447 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
448 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
449 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
450 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
451 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
452 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
453 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
454 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
455 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
456 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
457 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
458 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
459 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
460 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
461 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
462 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
463 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
464 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
465 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
466 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
467 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
468 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
469 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
470 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
471 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
472 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
473 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
474 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
475 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
476 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
477 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
478 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
479 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
480 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
481 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
482 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
483 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
484 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
485 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
486 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
487 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
488 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
489 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
490 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
491 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
492 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
493 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
494 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
495 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
496 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
497 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
498 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
499 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
500 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
501 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
502 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
503 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
504 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
505 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
506 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
507 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
508 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
509 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
510 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
511 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
512 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
513 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
514 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
515 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
516 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
517 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
518 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
519 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
520 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
521 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
522 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
523 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
524 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
525 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
526 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
527 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
528 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
529 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
530 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
531 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
532 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
533 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
534 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
535 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
536 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
537 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
538 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
539 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
540 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
541 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
542 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
543 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
544 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
545 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
546 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
547 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
548 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
549 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
550 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
551 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
552 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
553 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
554 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
555 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
556 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
557 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
558 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
559 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
560 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
561 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
562 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
563 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
564 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
565 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
566 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
567 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
568 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
569 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
570 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
571 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
572 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
573 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
574 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
575 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
576 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
577 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
578 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
579 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
580 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
581 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
582 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
583 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
584 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
585 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
586 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
587 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
588 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
589 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
590 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
591 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
592 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
593 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
594 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
595 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
596 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
597 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
598 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
599 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
600 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
601 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
602 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
603 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
604 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
605 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
606 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
607 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
608 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
609 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
610 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
611 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
612 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
613 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
614 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
615 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
616 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
617 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
618 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
619 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
620 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
621 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
622 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
623 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
624 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
625 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
626 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
627 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
628 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
629 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
630 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
631 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
632 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
633 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
634 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
635 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
636 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
637 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
638 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
639 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
640 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
641 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
642 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
643 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
644 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
645 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
646 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
647 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
648 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
649 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
650 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
651 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
652 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
653 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
654 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
655 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
656 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
657 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
658 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
659 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
660 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
661 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
662 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
663 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
664 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
665 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
666 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
667 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
668 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
669 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
670 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
671 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
672 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
673 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
674 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
675 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
676 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
677 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
678 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
679 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
680 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
681 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
682 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
683 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
684 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
685 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
686 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
687 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
688 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
689 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
690 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
691 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
692 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
693 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
694 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
695 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
696 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
697 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
698 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
699 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
700 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
701 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
702 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
703 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
704 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
705 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
706 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
707 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
708 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
709 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
710 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
711 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
712 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
713 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
714 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
715 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
716 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
717 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
718 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
719 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
720 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
721 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
722 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
723 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
724 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
725 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
726 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
727 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
728 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
729 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
730 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
731 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
732 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
733 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
734 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
735 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
736 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
737 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
738 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
739 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
740 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
741 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
742 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
743 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
744 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
745 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
746 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
747 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
748 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
749 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
750 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
751 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
752 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
753 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
754 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
755 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
756 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
757 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
758 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
759 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
760 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
761 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
762 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
763 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
764 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
765 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
766 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
767 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
768 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
769 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
770 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
771 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
772 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
773 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
774 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
775 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
776 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
777 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
778 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
779 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
780 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
781 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
782 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
783 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
784 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
785 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
786 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
787 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
788 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
789 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
790 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
791 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
792 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
793 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
794 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
795 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
796 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
797 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
798 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
799 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
800 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
801 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
802 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
803 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
804 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
805 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
806 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
807 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
808 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
809 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
810 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
811 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
812 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
813 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
814 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
815 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
816 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
817 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
818 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
819 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
820 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
821 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
822 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
823 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
824 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
825 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
826 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
827 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
828 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
829 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
830 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
831 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
832 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
833 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
834 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
835 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
836 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
837 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
838 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
839 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
840 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
841 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
842 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
843 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
844 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
845 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
846 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
847 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
848 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
849 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
850 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
851 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
852 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
853 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
854 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
855 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
856 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
857 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
858 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
859 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
860 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
861 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
862 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
863 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
864 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
865 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
866 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
867 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
868 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
869 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
870 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
871 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
872 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
873 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
874 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
875 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
876 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
877 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
878 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
879 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
880 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
881 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
882 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
883 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
884 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
885 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
886 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
887 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
888 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
889 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
890 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
891 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
892 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
893 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
894 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
895 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
896 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
897 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
898 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
899 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
900 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
901 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
902 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
903 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
904 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
905 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
906 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
907 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
908 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
909 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
910 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
911 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
912 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
913 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
914 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
915 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
916 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
917 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
918 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
919 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
920 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
921 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
922 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
923 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
924 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
925 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
926 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
927 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
928 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
929 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
930 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
931 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
932 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
933 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
934 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
935 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
936 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
937 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
938 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
939 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
940 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
941 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
942 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
943 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
944 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
945 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
946 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
947 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
948 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
949 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
950 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
951 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
952 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
953 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
954 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
955 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
956 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
957 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
958 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
959 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
960 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
961 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
962 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
963 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
964 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
965 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
966 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
967 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
968 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
969 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
970 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
971 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
972 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
973 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
974 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
975 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
976 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
977 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
978 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
979 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
980 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
981 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
982 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
983 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
984 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
985 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
986 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
987 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
988 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
989 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
990 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
991 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
992 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
993 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
994 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
995 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
996 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
997 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
998 FALSE ~ FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
+16794
View File
File diff suppressed because it is too large Load Diff
+523
View File
@@ -0,0 +1,523 @@
{
"start": {
"type": "question",
"inputType": "button",
"title": "What are you looking for?",
"subtitle": "Select the product category",
"notes": "<p><strong>Quick Tips:</strong></p><ul><li>Press <strong>Ctrl+Shift+R</strong> (Windows) or <strong>Cmd+Shift+R</strong> (Mac) to hard refresh the page</li><li>Press <strong>F5</strong> to reload the application</li><li>Use the back button or breadcrumbs to navigate</li></ul>",
"answers": [
{
"caption": "Window",
"image": "🪟",
"next": "q-dimensions"
},
{
"caption": "Door",
"image": "🚪",
"next": "q-dimensions"
},
{
"caption": "Patio Door",
"image": "🚪",
"next": "q-dimensions"
}
]
},
"q-dimensions": {
"type": "question",
"inputType": "form",
"title": "Select Storm Door Size",
"subtitle": "Choose a standard size or enter custom dimensions",
"notes": "<p><strong>Need help measuring?</strong></p><p>For accurate measurements, please refer to our <a href='https://columbiawindows.com/wp-content/uploads/2014/10/How-to-Measure2.pdf' target='_blank'>How to Measure Guide</a>.</p><p><strong>Tips:</strong></p><ul><li>Measure the rough opening, not the existing unit</li><li>Measure width at the top, middle, and bottom - use the smallest measurement</li><li>Measure height on the left, center, and right - use the smallest measurement</li><li>Round down to the nearest inch</li></ul>",
"measurementType": {
"defaultValue": "opening-size",
"options": [
{
"value": "opening-size",
"label": "Opening Size",
"disabled": false
},
{
"value": "tip-to-tip",
"label": "Tip-to-tip",
"disabled": false
}
]
},
"fields": [
{
"name": "width",
"label": "Width (inches)",
"type": "number",
"required": true,
"placeholder": "26"
},
{
"name": "height",
"label": "Height (inches)",
"type": "number",
"required": true,
"placeholder": "81"
}
],
"next": "q-material"
},
"q-material": {
"type": "question",
"inputType": "form",
"title": "Material & Color Preferences",
"subtitle": "Customize your selection",
"fields": [
{
"name": "material",
"label": "Material",
"type": "select",
"required": true,
"options": [
{ "value": "", "label": "-- Select Material --" },
{ "value": "vinyl", "label": "Vinyl" },
{ "value": "aluminum", "label": "Aluminum" }
]
},
{
"name": "color",
"label": "Color",
"type": "text",
"required": false,
"placeholder": "e.g., White, Beige, etc."
}
],
"next": "q-material-conditional"
},
"q-window-type": {
"type": "question",
"inputType": "button",
"title": "What type of window?",
"subtitle": "Select the category that best matches your needs",
"answers": [
{
"caption": "Single Hung Window",
"image": "🪟",
"next": "q-single-hung"
},
{
"caption": "Slider Window",
"image": "↔️",
"next": "q-slider"
},
{
"caption": "Fixed Lite",
"image": "🔲",
"next": "q-fixed"
},
{
"caption": "Casement Window",
"image": "🚪",
"next": "q-casement"
}
]
},
"q-door-type": {
"type": "question",
"inputType": "button",
"title": "What type of door?",
"subtitle": "Select your door style",
"answers": [
{
"caption": "Entry Door",
"image": "🚪",
"next": "prod-entry-door"
},
{
"caption": "Storm Door",
"image": "🚪",
"next": "prod-cobra-1100"
},
{
"caption": "Patio Door",
"image": "🚪",
"next": "q-patio"
}
]
},
"q-single-hung": {
"type": "question",
"inputType": "button",
"title": "What features do you need?",
"subtitle": "Choose your single hung window configuration",
"answers": [
{
"caption": "Vinyl Sliding Primary",
"image": "🪟",
"next": "prod-1400"
},
{
"caption": "Tilt Vinyl Primary",
"image": "🪟",
"next": "prod-1500"
},
{
"caption": "Insulated Single Hung",
"image": "🪟",
"next": "prod-1650"
},
{
"caption": "Thermal Break Tilt",
"image": "🪟",
"next": "prod-2000"
}
]
},
"q-slider": {
"type": "question",
"inputType": "button",
"title": "What type of slider do you need?",
"subtitle": "Select your slider window configuration",
"answers": [
{
"caption": "Insulated Slider",
"image": "↔️",
"next": "prod-1700"
},
{
"caption": "Thermal Break Slider",
"image": "↔️",
"next": "prod-2100"
},
{
"caption": "Single Glazed Slider",
"image": "↔️",
"next": "prod-2700"
},
{
"caption": "Replacement Slider",
"image": "↔️",
"next": "prod-3100"
}
]
},
"q-fixed": {
"type": "question",
"inputType": "button",
"title": "Which fixed lite option?",
"subtitle": "Choose your fixed window configuration",
"answers": [
{
"caption": "Vinyl Insulated Fixed",
"image": "🔲",
"next": "prod-1510"
},
{
"caption": "Thermal Break Fixed",
"image": "🔲",
"next": "prod-2200"
},
{
"caption": "Picture Window",
"image": "🔲",
"next": "prod-3200"
}
]
},
"q-casement": {
"type": "question",
"inputType": "button",
"title": "How many panels?",
"subtitle": "Select the number of casement panels",
"answers": [
{
"caption": "1-Panel Casement",
"image": "🚪",
"next": "prod-3300"
},
{
"caption": "2-Panel Casement",
"image": "🚪",
"next": "prod-3302"
},
{
"caption": "3-Panel Casement",
"image": "🚪",
"next": "prod-3303"
},
{
"caption": "Fixed Casement",
"image": "🚪",
"next": "prod-3310"
}
]
},
"q-patio": {
"type": "question",
"inputType": "button",
"title": "Select your patio door",
"subtitle": "Choose the right patio door for your space",
"answers": [
{
"caption": "Royal Crown Patio Door",
"image": "🚪",
"next": "prod-2400"
}
]
},
"prod-entry-door": {
"type": "product",
"code": "ENTRY-001",
"title": "Custom Entry Door",
"category": "DOOR",
"image": "https://images.unsplash.com/photo-1572025442646-866d16c84a54?w=400&h=400&fit=crop",
"url": "/products/entry-door",
"description": "Custom entry door based on your specifications.",
"features": [
"Custom dimensions",
"Choice of materials",
"Security features",
"Weather resistant"
]
},
"prod-cobra-1100": {
"type": "product",
"code": "1100",
"title": "Cobra Aluminum Storm Door",
"category": "STORM DOOR",
"image": "https://columbiawindows.com/wp-content/uploads/2014/10/Cobra-Ali-Storm-Door.jpg",
"url": "https://columbiawindows.com/product-detail/cobra-aluminum-storm-doors-1100/",
"description": "The Columbia Cobra aluminum storm door has a 1-1/4″ master frame. Double-track door with removable inserts. Available in black, bronze, sandstone or white baked on enamel finish.",
"features": [
"1-1/4″ master frame",
"Double-track with removable inserts",
"Baked on enamel finish",
"Available in Black, Bronze, Sandstone, White",
"Standard sizes: 2-6″ x 6-8″, 2-8″ x 6-8″, 3-0″ x 6-8″",
"Custom sizes available by special order"
],
"brochure": "https://columbiawindows.com./wp-content/uploads/2014/10/Accessories-08-18-2010s.pdf",
"measureGuide": "https://columbiawindows.com./wp-content/uploads/2014/10/How-to-Measure2.pdf"
},
"prod-1400": {
"type": "product",
"code": "1400",
"title": "Series 1400 Vinyl Sliding Primary",
"category": "SHPW",
"image": "https://images.unsplash.com/photo-1545259741-2ea3ebf61fa3?w=400&h=400&fit=crop",
"url": "/products/1400-vinyl-sliding",
"description": "High-quality vinyl sliding window with excellent insulation properties. Perfect for residential applications.",
"features": [
"Vinyl construction for durability",
"Smooth sliding operation",
"Energy efficient design",
"Low maintenance"
]
},
"prod-1500": {
"type": "product",
"code": "1500",
"title": "Series 1500 S.H. Tilt Vinyl Primary",
"category": "SHPT",
"url": "/products/1500-tilt-vinyl",
"description": "Single hung window with convenient tilt feature for easy cleaning.",
"features": [
"Tilt-in sashes for easy cleaning",
"Vinyl construction",
"Energy efficient",
"Security features"
]
},
"prod-1650": {
"type": "product",
"code": "1650",
"title": "#1650 Insulated Single Hung",
"category": "SHPW",
"url": "/products/1650-insulated",
"description": "Premium insulated single hung window for maximum energy efficiency.",
"features": [
"Superior insulation",
"Double-pane glass",
"Weatherstripping",
"Durable vinyl frame"
]
},
"prod-2000": {
"type": "product",
"code": "2000",
"title": "Series 2000 S.H. T.B. Tilt Primary",
"category": "SHPT",
"url": "/products/2000-thermal-break",
"description": "Thermal break single hung window with tilt feature.",
"features": [
"Thermal break technology",
"Tilt-in sashes",
"Enhanced energy efficiency",
"Condensation resistant"
]
},
"prod-1700": {
"type": "product",
"code": "1700",
"title": "#1700 Insulated Slider",
"category": "SHPW",
"url": "/products/1700-slider",
"description": "Insulated sliding window with smooth operation.",
"features": [
"Insulated glass",
"Smooth gliding action",
"Multiple vent configurations",
"Screens available"
]
},
"prod-2100": {
"type": "product",
"code": "2100",
"title": "Series 2100 Thermal Break Slider",
"category": "SHPT",
"url": "/products/2100-thermal-slider",
"description": "Advanced thermal break slider for superior performance.",
"features": [
"Thermal break construction",
"Energy Star rated",
"Heavy-duty rollers",
"Available in 3-panel configuration"
]
},
"prod-2700": {
"type": "product",
"code": "2700",
"title": "#2700 Single Glazed Slider",
"category": "SHPW",
"url": "/products/2700-single-glazed",
"description": "Single glazed sliding window for standard applications.",
"features": [
"Cost-effective solution",
"Reliable operation",
"Multiple configurations",
"Durable construction"
]
},
"prod-3100": {
"type": "product",
"code": "3100",
"title": "Series 3100 T.B. Replacement Slider",
"category": "3000DHP",
"url": "/products/3100-replacement",
"description": "Thermal break replacement slider for existing window openings.",
"features": [
"Replacement window design",
"Thermal break technology",
"Easy installation",
"Energy efficient"
]
},
"prod-1510": {
"type": "product",
"code": "1510",
"title": "1510 Vinyl Insulated Fixed Lite",
"category": "FPPW",
"url": "/products/1510-fixed-lite",
"description": "Fixed window panel with insulated glass.",
"features": [
"Non-operable design",
"Insulated glass",
"Vinyl frame",
"Low maintenance"
]
},
"prod-2200": {
"type": "product",
"code": "2200",
"title": "Series 2200 T.B. Fixed Lite",
"category": "FPPW",
"url": "/products/2200-thermal-fixed",
"description": "Thermal break fixed lite window for maximum efficiency.",
"features": [
"Thermal break frame",
"Fixed glass panel",
"Superior insulation",
"Tempered glass option available"
]
},
"prod-3200": {
"type": "product",
"code": "3200",
"title": "Series 3200 T.B. Picture Window",
"category": "3000FPP",
"url": "/products/3200-picture-window",
"description": "Large picture window with thermal break technology.",
"features": [
"Expansive glass area",
"Thermal break construction",
"Unobstructed views",
"Energy efficient"
]
},
"prod-3300": {
"type": "product",
"code": "3300",
"title": "3300 1-Panel T.B. Casement",
"category": "CASEMNT",
"url": "/products/3300-casement-1panel",
"description": "Single panel casement window with thermal break.",
"features": [
"Hinged operation",
"Thermal break frame",
"Full opening capability",
"Secure locking system"
]
},
"prod-3302": {
"type": "product",
"code": "3302",
"title": "3300 2-Panel T.B. Casement",
"category": "CASEMNT",
"url": "/products/3302-casement-2panel",
"description": "Two-panel casement window configuration.",
"features": [
"Dual panels",
"Thermal break technology",
"Flexible ventilation",
"Energy efficient"
]
},
"prod-3303": {
"type": "product",
"code": "3303",
"title": "3300 3-Panel T.B. Casement",
"category": "CASEMNT",
"url": "/products/3303-casement-3panel",
"description": "Three-panel casement window for larger openings.",
"features": [
"Triple panel design",
"Maximum ventilation",
"Thermal break construction",
"Architectural appeal"
]
},
"prod-3310": {
"type": "product",
"code": "3310",
"title": "3310 Fixed Casement",
"category": "CASEMNT",
"url": "/products/3310-fixed-casement",
"description": "Fixed casement window panel.",
"features": [
"Non-operable design",
"Casement styling",
"Energy efficient",
"Durable construction"
]
},
"prod-2400": {
"type": "product",
"code": "2400",
"title": "2400 Royal Crown Patio Door",
"category": "PD",
"image": "https://images.unsplash.com/photo-1585412727339-b82d6d394f51?w=400&h=400&fit=crop",
"url": "/products/2400-patio-door",
"description": "Premium patio door system with superior performance.",
"features": [
"Wide opening",
"Smooth operation",
"Security features",
"Energy efficient glass"
]
}
}
+98
View File
@@ -0,0 +1,98 @@
[
{
"username": "Master",
"password": "pbkdf2:sha256:1000000$tPRmseRGKLveXTxS$87eda9a20db29d12838fcb607f1f3536141f743ec9879702dc2a0dd893b4078d",
"defaultLocation": "KC",
"permissions": {
"manage_users": true,
"view_reports": true,
"create_quotes": true,
"approve_quotes": true,
"manage_products": true,
"manage_inventory": true
},
"locationSettings": {
"LINDS": {
"accessible": true
},
"IOLA": {
"accessible": true
},
"KC": {
"accessible": true
},
"BMD": {
"accessible": true
}
},
"active": true
},
{
"username": "Darlene",
"password": "pbkdf2:sha256:1000000$8sblSWLWIvsjcta2$d974d13956f67cc42e1fa88191d68c896d4daa663ae2d76d7988beb569647d27",
"defaultLocation": "IOLA",
"permissions": {
"manage_users": false,
"view_reports": true,
"create_quotes": true,
"approve_quotes": false,
"manage_products": false,
"manage_inventory": false
},
"locationSettings": {
"LINDS": {
"accessible": true
},
"IOLA": {
"accessible": true
},
"KC": {
"accessible": true
},
"BMD": {
"accessible": false
}
},
"active": true
},
{
"username": "Toni",
"password": "pbkdf2:sha256:1000000$5k3daJKZt7hqNAkq$e77819a2b2ee4bf9955dc10b3b4da7b14de2b4ecda5ce5f553daf3783b0a316c",
"defaultLocation": "LINDS",
"locationSettings": {
"LINDS": {
"accessible": true
},
"IOLA": {
"accessible": false
},
"KC": {
"accessible": false
},
"BMD": {
"accessible": false
}
},
"active": true
},
{
"username": "Jason",
"password": "pbkdf2:sha256:1000000$rNSYLqRWZ6tdny1G$cad2afda9193b7cb50b9e9423f95ff83bc14428c8a2d98607cd6e5ce17cb70c7",
"defaultLocation": "KC",
"locationSettings": {
"LINDS": {
"accessible": true
},
"IOLA": {
"accessible": true
},
"KC": {
"accessible": true
},
"BMD": {
"accessible": true
}
},
"active": false
}
]
+111
View File
@@ -0,0 +1,111 @@
"""
Debug startup script - writes detailed logs to file
Upload this and temporarily set it as the startup file to diagnose issues
"""
import sys
import os
from datetime import datetime
# Create log file
log_file = '/home/bmdwtjuw/product-finder/startup_debug.log'
def log(message):
with open(log_file, 'a') as f:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
f.write(f"[{timestamp}] {message}\n")
try:
log("="*70)
log("STARTUP DEBUG - BEGIN")
log("="*70)
# Python info
log(f"Python version: {sys.version}")
log(f"Python executable: {sys.executable}")
log(f"Current directory: {os.getcwd()}")
# Environment variables
log("\nEnvironment Variables:")
log(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
log(f" FLASK_ENV: {os.environ.get('FLASK_ENV', 'NOT SET')}")
# Python path
log(f"\nPython path: {sys.path}")
# Check Flask installation
log("\nChecking Flask installation...")
try:
import flask
log(f" ✓ Flask version: {flask.__version__ if hasattr(flask, '__version__') else 'unknown'}")
except ImportError as e:
log(f" ✗ Flask NOT installed: {e}")
log(" RUN: pip install Flask==3.0.0 Werkzeug==3.0.1")
# Check Werkzeug
try:
import werkzeug
log(f" ✓ Werkzeug version: {werkzeug.__version__ if hasattr(werkzeug, '__version__') else 'unknown'}")
except ImportError as e:
log(f" ✗ Werkzeug NOT installed: {e}")
# Check if app.py exists
log("\nChecking files...")
app_py_path = os.path.join(os.path.dirname(__file__), 'app.py')
if os.path.exists(app_py_path):
log(f" ✓ app.py exists at {app_py_path}")
else:
log(f" ✗ app.py NOT FOUND at {app_py_path}")
# Try importing app
log("\nAttempting to import Flask app...")
try:
from app import app
log(f" ✓ Successfully imported Flask app")
log(f" APPLICATION_ROOT config: {app.config.get('APPLICATION_ROOT', 'NOT SET')}")
# Count routes
routes = list(app.url_map.iter_rules())
log(f" ✓ Registered routes: {len(routes)}")
# Create WSGI application
def application(environ, start_response):
log(f"\nRequest received: {environ.get('PATH_INFO', '/')}")
return app(environ, start_response)
log("\n✓ APPLICATION READY - Check site now")
log("="*70)
except Exception as e:
log(f" ✗ Failed to import app:")
log(f" Error: {e}")
import traceback
log(f"\nFull traceback:")
log(traceback.format_exc())
# Create error application
def application(environ, start_response):
status = '500 Internal Server Error'
output = f'Import Error - Check {log_file}\n\n{traceback.format_exc()}'.encode('utf-8')
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
log("="*70)
except Exception as e:
# Catch-all for any errors
import traceback
log(f"\nFATAL ERROR during startup:")
log(str(e))
log(traceback.format_exc())
log("="*70)
def application(environ, start_response):
status = '500 Internal Server Error'
output = f'Startup Error - Check {log_file}'.encode('utf-8')
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Fix Passenger Configuration
============================
This script renames the corrupted passenger_wsgi.py file so Passenger
will use the control panel settings instead.
"""
import os
import sys
def main():
print("=" * 70)
print("FIXING PASSENGER CONFIGURATION")
print("=" * 70)
# Check if passenger_wsgi.py exists
if os.path.exists('passenger_wsgi.py'):
print("\nFound passenger_wsgi.py - this file is causing the problem")
print("Renaming it to passenger_wsgi.py.DISABLED...")
try:
os.rename('passenger_wsgi.py', 'passenger_wsgi.py.DISABLED')
print("SUCCESS: passenger_wsgi.py renamed to passenger_wsgi.py.DISABLED")
print("\nPassenger will now use the control panel settings:")
print(" Application startup file: wsgi.py")
print(" Application Entry point: app")
except Exception as e:
print(f"ERROR: Could not rename file: {e}")
return 1
else:
print("\npassenger_wsgi.py not found - already fixed or doesn't exist")
print("\n" + "=" * 70)
print("NEXT STEPS:")
print("=" * 70)
print("1. Restart your Python application in the control panel")
print("2. Visit: https://columbiawindows.com/product-finder/test")
print("3. If it works, you're done!")
print("=" * 70)
return 0
if __name__ == '__main__':
sys.exit(main())
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# Fix Passenger lock file permissions
# Run this on the server to resolve "Can't acquire lock" error
echo "=========================================="
echo "FIXING PASSENGER LOCK FILE ISSUES"
echo "=========================================="
echo ""
# Navigate to app directory
cd ~/product-finder
# Remove any stale lock files
echo "🧹 Cleaning stale lock files..."
if [ -d "tmp" ]; then
rm -rf tmp/*
echo " ✓ tmp directory cleaned"
else
echo " ⚠️ tmp directory doesn't exist - will create it"
fi
# Create tmp directory with correct permissions
echo ""
echo "📁 Setting up tmp directory..."
mkdir -p tmp
chmod 755 tmp
echo " ✓ tmp directory created with 755 permissions"
# Create restart file
echo ""
echo "🔄 Restarting Passenger..."
touch tmp/restart.txt
chmod 644 tmp/restart.txt
echo " ✓ tmp/restart.txt created"
# Set correct permissions for app files
echo ""
echo "🔒 Setting file permissions..."
chmod 755 .
chmod 644 passenger_wsgi.py
chmod 644 app.py
chmod 644 .htaccess
chmod -R 755 data 2>/dev/null || mkdir -p data && chmod 755 data
chmod -R 755 templates
chmod -R 755 static 2>/dev/null || true
echo " ✓ File permissions set"
# Show current tmp directory status
echo ""
echo "📊 Current tmp directory status:"
ls -la tmp/
echo ""
echo "=========================================="
echo "✓ LOCK FILE ISSUE FIXED"
echo "=========================================="
echo ""
echo "Next steps:"
echo "1. Wait 30 seconds for Passenger to restart"
echo "2. Test: https://columbiawindows.com/product-finder/"
echo ""
echo "If still failing, check error log:"
echo "tail -50 ~/logs/error_log"
echo ""
+295
View File
@@ -0,0 +1,295 @@
"""
Generate Bitwise Helper File for Product Classification
Creates a helper file with bitwise flags based on product attributes (columns G-S)
"""
import csv
import json
from collections import defaultdict
# Bit position definitions
BIT_DEFINITIONS = {
# Base Type (Bits 0-1)
'base_door': 0, # 2^0 = 1
'base_window': 1, # 2^1 = 2
# Product Flags (Bits 2-3)
'is_accessory': 2, # 2^2 = 4
'specific_item': 3, # 2^3 = 8
# Materials (Bits 4-5)
'material_aluminum': 4, # 2^4 = 16
'material_vinyl': 5, # 2^5 = 32
# Colors (Bits 6-11)
'color_black': 6, # 2^6 = 64
'color_white': 7, # 2^7 = 128
'color_bronze': 8, # 2^8 = 256
'color_tan': 9, # 2^9 = 512
'color_mill': 10, # 2^10 = 1024
'color_sandstone': 11, # 2^11 = 2048
}
# Sub-type bits will be assigned dynamically (starting at bit 12)
SUBTYPE_BIT_START = 12
def parse_bool(value):
"""Parse TRUE/FALSE string to boolean"""
if isinstance(value, str):
return value.strip().upper() == 'TRUE'
return bool(value)
def calculate_bit_value(row, subtype_map):
"""Calculate the bitwise value for a product row"""
bit_value = 0
explanation = []
# Base Type (Column G - index 6)
base_type = row[6].strip() if len(row) > 6 and row[6] else ""
if base_type.lower() == 'door':
bit_value |= (1 << BIT_DEFINITIONS['base_door'])
explanation.append('Door')
elif base_type.lower() == 'window':
bit_value |= (1 << BIT_DEFINITIONS['base_window'])
explanation.append('Window')
# Accessory Flag (Column J - index 9)
if len(row) > 9 and parse_bool(row[9]):
bit_value |= (1 << BIT_DEFINITIONS['is_accessory'])
explanation.append('Accessory')
# Specific Item Link (Column K - index 10)
if len(row) > 10 and parse_bool(row[10]):
bit_value |= (1 << BIT_DEFINITIONS['specific_item'])
explanation.append('SpecificItem')
# Materials
if len(row) > 11 and parse_bool(row[11]): # Aluminum (Column L)
bit_value |= (1 << BIT_DEFINITIONS['material_aluminum'])
explanation.append('Aluminum')
if len(row) > 12 and parse_bool(row[12]): # Vinyl (Column M)
bit_value |= (1 << BIT_DEFINITIONS['material_vinyl'])
explanation.append('Vinyl')
# Colors
color_columns = [
(13, 'color_black', 'Black'),
(14, 'color_white', 'White'),
(15, 'color_bronze', 'Bronze'),
(16, 'color_tan', 'Tan'),
(17, 'color_mill', 'Mill'),
(18, 'color_sandstone', 'Sandstone'),
]
for col_idx, bit_key, color_name in color_columns:
if len(row) > col_idx and parse_bool(row[col_idx]):
bit_value |= (1 << BIT_DEFINITIONS[bit_key])
explanation.append(color_name)
# Sub-types (Columns H and I - indices 7 and 8)
door_subtype = row[7].strip() if len(row) > 7 and row[7] else ""
window_subtype = row[8].strip() if len(row) > 8 and row[8] else ""
if door_subtype and door_subtype in subtype_map:
bit_pos = subtype_map[door_subtype]
bit_value |= (1 << bit_pos)
explanation.append(f'Subtype:{door_subtype}')
if window_subtype and window_subtype in subtype_map:
bit_pos = subtype_map[window_subtype]
bit_value |= (1 << bit_pos)
explanation.append(f'Subtype:{window_subtype}')
return bit_value, explanation
def collect_subtypes(csv_path):
"""Collect all unique sub-types from the CSV"""
subtypes = set()
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
next(reader) # Skip first header row
next(reader) # Skip second header row
for row in reader:
if len(row) < 8:
continue
# Skip discontinued items
if len(row) > 0 and parse_bool(row[0]):
continue
# Collect door subtype (column H - index 7)
if len(row) > 7 and row[7].strip():
subtypes.add(row[7].strip())
# Collect window subtype (column I - index 8)
if len(row) > 8 and row[8].strip():
subtypes.add(row[8].strip())
return sorted(subtypes)
def generate_helper_files(csv_path):
"""Generate helper CSV and JSON files with bitwise values"""
print("Analyzing product data...")
# First pass: collect all unique sub-types
subtypes = collect_subtypes(csv_path)
# Create subtype bit mapping
subtype_map = {}
for idx, subtype in enumerate(subtypes):
subtype_map[subtype] = SUBTYPE_BIT_START + idx
print(f"Found {len(subtypes)} unique sub-types")
# Second pass: calculate bit values
results = []
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
next(reader) # Skip first header row
next(reader) # Skip second header row
for row in reader:
if len(row) < 3:
continue
prod_code = row[2].strip() # Column C - PROD_CODE
discontinued = parse_bool(row[0]) # Column A - DISCONT
bit_value, explanation = calculate_bit_value(row, subtype_map)
results.append({
'PROD_CODE': prod_code,
'BIT_VALUE': bit_value,
'BIT_HEX': f"0x{bit_value:X}",
'BIT_BINARY': f"{bit_value:b}",
'DISCONTINUED': discontinued,
'FLAGS': ' | '.join(explanation) if explanation else 'None'
})
# Filter out discontinued products for output files
active_results = [r for r in results if not r['DISCONTINUED']]
print(f"Total products processed: {len(results)}")
print(f"Active products: {len(active_results)}")
print(f"Discontinued products (filtered out): {len(results) - len(active_results)}")
# Write CSV helper file (only active products)
csv_output = 'data/product_bitwise.csv'
with open(csv_output, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['PROD_CODE', 'BIT_VALUE', 'BIT_HEX', 'BIT_BINARY', 'DISCONTINUED', 'FLAGS'])
writer.writeheader()
writer.writerows(active_results)
print(f"✓ Created {csv_output}")
# Write JSON helper file (only active products)
json_output = 'data/product_bitwise.json'
with open(json_output, 'w', encoding='utf-8') as f:
json.dump(active_results, f, indent=2)
print(f"✓ Created {json_output}")
# Create bit legend/documentation
legend = {
'description': 'Bitwise flag system for product classification',
'bit_definitions': {
'base_attributes': {
'bit_0 (1)': 'Base Type = Door',
'bit_1 (2)': 'Base Type = Window',
'bit_2 (4)': 'Is Accessory',
'bit_3 (8)': 'Specific Item Link',
},
'materials': {
'bit_4 (16)': 'Material = Aluminum',
'bit_5 (32)': 'Material = Vinyl',
},
'colors': {
'bit_6 (64)': 'Color = Black',
'bit_7 (128)': 'Color = White',
'bit_8 (256)': 'Color = Bronze',
'bit_9 (512)': 'Color = Tan',
'bit_10 (1024)': 'Color = Mill',
'bit_11 (2048)': 'Color = Sandstone',
},
'subtypes': {}
},
'usage_examples': {
'check_if_door': 'bit_value & 1',
'check_if_window': 'bit_value & 2',
'check_if_aluminum': 'bit_value & 16',
'check_if_white': 'bit_value & 128',
'check_multiple': '(bit_value & 1) and (bit_value & 16) # Door AND Aluminum',
}
}
# Add subtype mappings to legend
for subtype, bit_pos in sorted(subtype_map.items(), key=lambda x: x[1]):
bit_value = 1 << bit_pos
legend['bit_definitions']['subtypes'][f'bit_{bit_pos} ({bit_value})'] = f'Subtype = {subtype}'
legend['usage_examples'][f'check_subtype_{subtype.replace(" ", "_").lower()}'] = f'bit_value & {bit_value}'
# Write legend file
legend_output = 'data/bitwise_legend.json'
with open(legend_output, 'w', encoding='utf-8') as f:
json.dump(legend, f, indent=2)
print(f"✓ Created {legend_output}")
# Generate statistics
stats = {
'total_products': len([r for r in results if not r['DISCONTINUED']]),
'total_discontinued': len([r for r in results if r['DISCONTINUED']]),
'total_accessories': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 4)]),
'doors': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 1)]),
'windows': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 2)]),
'aluminum_products': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 16)]),
'vinyl_products': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 32)]),
'multi_material': len([r for r in results if not r['DISCONTINUED'] and (r['BIT_VALUE'] & 16) and (r['BIT_VALUE'] & 32)]),
}
print("\n" + "="*60)
print("STATISTICS")
print("="*60)
print(f"Total Active Products: {stats['total_products']}")
print(f"Total Discontinued: {stats['total_discontinued']}")
print(f"Accessories: {stats['total_accessories']}")
print(f"Doors: {stats['doors']}")
print(f"Windows: {stats['windows']}")
print(f"Aluminum Products: {stats['aluminum_products']}")
print(f"Vinyl Products: {stats['vinyl_products']}")
print(f"Multi-Material Products: {stats['multi_material']}")
print(f"Unique Sub-types: {len(subtypes)}")
print("="*60)
# Show example products
print("\nEXAMPLE PRODUCTS:")
print("-"*60)
for i, result in enumerate(active_results[:5]):
print(f"{result['PROD_CODE']:12} | {result['BIT_VALUE']:6} | {result['BIT_HEX']:8} | {result['FLAGS']}")
print("-"*60)
return active_results, legend, subtype_map
if __name__ == '__main__':
csv_path = 'data/products.csv'
try:
results, legend, subtype_map = generate_helper_files(csv_path)
print("\n✓ All helper files generated successfully!")
print("\nGenerated files:")
print(" - data/product_bitwise.csv (CSV format with bit values)")
print(" - data/product_bitwise.json (JSON format with bit values)")
print(" - data/bitwise_legend.json (Bit mapping documentation)")
except FileNotFoundError:
print(f"Error: Could not find {csv_path}")
print("Make sure products.csv exists in the data/ folder")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
+297
View File
@@ -0,0 +1,297 @@
"""
Dynamic Product Image Generator
Generates product images with configurable colors, hardware positions, and other options.
Uses PIL/Pillow for image manipulation.
"""
import os
import json
import hashlib
from io import BytesIO
from PIL import Image, ImageDraw, ImageEnhance, ImageColor
from pathlib import Path
class ProductImageGenerator:
"""Handles dynamic generation of product images based on configuration."""
def __init__(self, config_file='data/image_configs.json'):
"""Initialize with configuration file."""
self.config_file = config_file
self.configs = self._load_configs()
self.cache_dir = Path('cache/product_images')
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _load_configs(self):
"""Load image configuration from JSON file."""
try:
with open(self.config_file, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Error loading image configs: {e}")
return {}
def _find_image(self, image_path):
"""
Find image file, trying different extensions if needed.
Args:
image_path: Path to image (e.g., 'images/cobrai.jpg')
Returns:
Actual path to image file, or None if not found
"""
# Try the exact path first
if os.path.exists(image_path):
return image_path
# Get base path without extension
base_path = os.path.splitext(image_path)[0]
# Try common image extensions
for ext in ['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG']:
test_path = base_path + ext
if os.path.exists(test_path):
return test_path
return None
def generate_product_image(self, product_code, color='white', hinge='right',
material='aluminum', use_cache=True):
"""
Generate a product image with specified configuration.
Args:
product_code: Product identifier (e.g., 'cobrai')
color: Color name (e.g., 'white', 'black', 'bronze')
hinge: Hinge side ('right' or 'left')
material: Material type (for future use)
use_cache: Whether to use cached images
Returns:
PIL Image object
"""
product_code = product_code.lower()
# Check if product config exists
if product_code not in self.configs:
raise ValueError(f"Product {product_code} not found in configuration")
config = self.configs[product_code]
# Check cache first
if use_cache:
cache_key = self._get_cache_key(product_code, color, hinge, material)
cached_image = self._get_cached_image(cache_key)
if cached_image:
return cached_image
# Load base image
base_image_path = config['sourceImage']
# Try to find the image with different extensions if needed
image_path = self._find_image(base_image_path)
if not image_path:
raise FileNotFoundError(f"Could not load base image: {base_image_path}. Tried .jpg, .png, .jpeg")
try:
base_image = Image.open(image_path).convert('RGBA')
except Exception as e:
raise FileNotFoundError(f"Could not load base image: {image_path}. Error: {e}")
# Apply color to regions
if color in config['availableColors']:
color_rgb = tuple(config['availableColors'][color]['rgb'])
base_image = self._apply_color_to_regions(
base_image,
config['colorableRegions'],
color_rgb
)
# Apply hardware (handles, locks)
if hinge in config.get('hardwarePositions', {}):
base_image = self._apply_hardware(
base_image,
config['hardwarePositions'],
hinge
)
# Flip image if left hinge
if hinge == 'left':
base_image = base_image.transpose(Image.FLIP_LEFT_RIGHT)
# Cache the result
if use_cache:
self._cache_image(cache_key, base_image)
return base_image
def _apply_color_to_regions(self, image, regions, target_color):
"""
Apply color to specific regions of the image.
This method attempts to recolor the frame/panel areas while preserving
shadows, highlights, and texture.
"""
img_copy = image.copy()
pixels = img_copy.load()
width, height = img_copy.size
# Get the average brightness of the target color
target_brightness = sum(target_color) / 3
for region in regions:
x1, y1 = region['topLeft']
x2, y2 = region['bottomRight']
# Ensure coordinates are within bounds
x1 = max(0, min(x1, width - 1))
x2 = max(0, min(x2, width))
y1 = max(0, min(y1, height - 1))
y2 = max(0, min(y2, height))
# Apply color to region while preserving luminosity
for y in range(y1, y2):
for x in range(x1, x2):
try:
r, g, b, a = pixels[x, y]
# Calculate original brightness
original_brightness = (r + g + b) / 3
# Preserve relative brightness
if original_brightness > 0:
brightness_factor = original_brightness / 255.0
# Apply target color with brightness preservation
new_r = int(target_color[0] * brightness_factor)
new_g = int(target_color[1] * brightness_factor)
new_b = int(target_color[2] * brightness_factor)
# Ensure values are in valid range
new_r = max(0, min(255, new_r))
new_g = max(0, min(255, new_g))
new_b = max(0, min(255, new_b))
pixels[x, y] = (new_r, new_g, new_b, a)
except IndexError:
continue
return img_copy
def _apply_hardware(self, image, hardware_positions, hinge_side):
"""Apply hardware (handles, locks) to the image."""
img_copy = image.copy()
hardware_key = f"handle_{hinge_side}"
if hardware_key in hardware_positions:
hardware_config = hardware_positions[hardware_key]
hardware_path = hardware_config.get('image')
if hardware_path:
# Try to find the hardware image with different extensions
actual_path = self._find_image(hardware_path)
if actual_path:
try:
hardware_img = Image.open(actual_path).convert('RGBA')
# Flip if needed
if hardware_config.get('flip', False):
hardware_img = hardware_img.transpose(Image.FLIP_LEFT_RIGHT)
# Paste hardware at specified position
x, y = hardware_config['x'], hardware_config['y']
img_copy.paste(hardware_img, (x, y), hardware_img)
except Exception as e:
print(f"Could not apply hardware: {e}")
return img_copy
def _get_cache_key(self, product_code, color, hinge, material):
"""Generate a unique cache key for the configuration."""
key_string = f"{product_code}_{color}_{hinge}_{material}"
return hashlib.md5(key_string.encode()).hexdigest()
def _get_cached_image(self, cache_key):
"""Retrieve cached image if available."""
cache_path = self.cache_dir / f"{cache_key}.png"
if cache_path.exists():
try:
return Image.open(cache_path)
except Exception:
return None
return None
def _cache_image(self, cache_key, image):
"""Save image to cache."""
cache_path = self.cache_dir / f"{cache_key}.png"
try:
image.save(cache_path, 'PNG', optimize=True)
except Exception as e:
print(f"Could not cache image: {e}")
def get_product_config(self, product_code):
"""Get configuration for a specific product."""
return self.configs.get(product_code.lower())
def clear_cache(self, product_code=None):
"""Clear cached images. If product_code provided, only clear that product's cache."""
if product_code:
# Clear specific product cache (would need to track cache keys)
pass
else:
# Clear all cache
for cache_file in self.cache_dir.glob('*.png'):
try:
cache_file.unlink()
except Exception:
pass
def recolor_image_advanced(image, source_color_range, target_color):
"""
Advanced recoloring that replaces pixels within a color range.
Args:
image: PIL Image
source_color_range: Dict with 'min' and 'max' RGB tuples
target_color: Target RGB tuple
"""
img_copy = image.copy()
pixels = img_copy.load()
width, height = img_copy.size
min_r, min_g, min_b = source_color_range['min']
max_r, max_g, max_b = source_color_range['max']
for y in range(height):
for x in range(width):
r, g, b, a = pixels[x, y]
# Check if pixel is within source color range
if (min_r <= r <= max_r and
min_g <= g <= max_g and
min_b <= b <= max_b):
# Calculate brightness factor
brightness = (r + g + b) / 3 / 255.0
# Apply target color with brightness
new_r = int(target_color[0] * brightness)
new_g = int(target_color[1] * brightness)
new_b = int(target_color[2] * brightness)
pixels[x, y] = (new_r, new_g, new_b, a)
return img_copy
def image_to_base64(image, format='PNG'):
"""Convert PIL Image to base64 string."""
import base64
buffered = BytesIO()
image.save(buffered, format=format)
img_str = base64.b64encode(buffered.getvalue()).decode()
return f"data:image/{format.lower()};base64,{img_str}"
Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

+451
View File
@@ -0,0 +1,451 @@
/**
* Product Encoding/Decoding Helper
*
* Provides functions to encode and decode product configurations into compact
* hex-based URL strings. Uses bit-packing for efficient, stable encoding.
*
* Format: v-T-C-M-HHHH-HHHH-HHHH
* See /planning/ENCODING_SYSTEM.md for detailed documentation
*/
// ============================================================================
// LOOKUP TABLES
// ============================================================================
const ENCODING_VERSION = 0;
// Main Product Attributes (single hex character, 0-15)
const PRODUCT_TYPE = {
'PATIO_DOOR': 1,
'STORM_DOOR': 3,
'STORM_WINDOW': 5,
'PRIMARY_WINDOW': 7
};
const PRODUCT_TYPE_REVERSE = {
1: 'PATIO_DOOR',
3: 'STORM_DOOR',
5: 'STORM_WINDOW',
7: 'PRIMARY_WINDOW'
};
const PRODUCT_COLOR = {
'BLACK': 1,
'BRONZE': 2,
'SANDSTONE': 3,
'WHITE': 4,
'TAN': 5,
'MILL': 6
};
const PRODUCT_COLOR_REVERSE = {
1: 'BLACK',
2: 'BRONZE',
3: 'SANDSTONE',
4: 'WHITE',
5: 'TAN',
6: 'MILL'
};
const PRODUCT_MATERIAL = {
'ALUMINUM': 1,
'VINYL': 2
};
const PRODUCT_MATERIAL_REVERSE = {
1: 'ALUMINUM',
2: 'VINYL'
};
// Hardware Fields (5 bits each, 0-31)
const HARDWARE_TYPE = {
'NONE': 0,
'LEVER': 1,
'PULL': 2,
'PULL_HANDLE': 3,
'DEADBOLT': 4,
'HINGE': 5
};
const HARDWARE_TYPE_REVERSE = {
0: 'NONE',
1: 'LEVER',
2: 'PULL',
3: 'PULL_HANDLE',
4: 'DEADBOLT',
5: 'HINGE'
};
const HARDWARE_STYLE = {
'NONE': 0,
'STANDARD': 1,
'PUSH': 2,
'ALTERNATIVE': 3,
'CONTEMPORARY': 4,
'TRADITIONAL': 5
};
const HARDWARE_STYLE_REVERSE = {
0: 'NONE',
1: 'STANDARD',
2: 'PUSH',
3: 'ALTERNATIVE',
4: 'CONTEMPORARY',
5: 'TRADITIONAL'
};
const HARDWARE_COLOR = {
'NONE': 0,
'BRASS': 1,
'WHITE': 2,
'BLACK': 3,
'SATIN': 4,
'NICKEL': 5,
'BRONZE': 6
};
const HARDWARE_COLOR_REVERSE = {
0: 'NONE',
1: 'BRASS',
2: 'WHITE',
3: 'BLACK',
4: 'SATIN',
5: 'NICKEL',
6: 'BRONZE'
};
// ============================================================================
// ENCODING FUNCTIONS
// ============================================================================
/**
* Encode a hardware configuration into a 4-character hex string
* Uses 5 bits per field (32 values each)
*
* @param {number} type - Hardware type (0-31)
* @param {number} style - Hardware style (0-31)
* @param {number} color - Hardware color (0-31)
* @returns {string} 4-character hex string (e.g., "0421")
*/
function encodeHardware(type, style, color) {
// Validate inputs
if (type < 0 || type > 31) throw new Error(`Invalid hardware type: ${type} (must be 0-31)`);
if (style < 0 || style > 31) throw new Error(`Invalid hardware style: ${style} (must be 0-31)`);
if (color < 0 || color > 31) throw new Error(`Invalid hardware color: ${color} (must be 0-31)`);
// Pack into 16 bits: [reserved(1)][type(5)][style(5)][color(5)]
const value = ((type & 0x1F) << 10) | // Bits 10-14
((style & 0x1F) << 5) | // Bits 5-9
(color & 0x1F); // Bits 0-4
return value.toString(16).toUpperCase().padStart(4, '0');
}
/**
* Encode a complete product configuration into a URL string
*
* @param {Object} config - Product configuration
* @param {number} config.type - Product type (0-15)
* @param {number} config.color - Product color (0-15)
* @param {number} config.material - Product material (0-15)
* @param {Array<Object>} config.hardware - Array of hardware items (optional)
* @param {number} config.hardware[].type - Hardware type (0-31)
* @param {number} config.hardware[].style - Hardware style (0-31)
* @param {number} config.hardware[].color - Hardware color (0-31)
* @returns {string} Encoded URL string (e.g., "0-1-4-1-0421")
*/
function encodeProduct(config) {
// Validate main attributes
if (config.type < 0 || config.type > 15) {
throw new Error(`Invalid product type: ${config.type} (must be 0-15)`);
}
if (config.color < 0 || config.color > 15) {
throw new Error(`Invalid product color: ${config.color} (must be 0-15)`);
}
if (config.material < 0 || config.material > 15) {
throw new Error(`Invalid product material: ${config.material} (must be 0-15)`);
}
// Build base string: version-type-color-material
const parts = [
ENCODING_VERSION.toString(16).toUpperCase(),
config.type.toString(16).toUpperCase(),
config.color.toString(16).toUpperCase(),
config.material.toString(16).toUpperCase()
];
// Add hardware items if present
if (config.hardware && config.hardware.length > 0) {
config.hardware.forEach(hw => {
parts.push(encodeHardware(hw.type, hw.style, hw.color));
});
}
return parts.join('-');
}
/**
* Encode using named constants (convenience function)
*
* @param {Object} config - Product configuration with named values
* @param {string} config.type - Product type name (e.g., 'PATIO_DOOR')
* @param {string} config.color - Color name (e.g., 'WHITE')
* @param {string} config.material - Material name (e.g., 'ALUMINUM')
* @param {Array<Object>} config.hardware - Array of hardware items (optional)
* @returns {string} Encoded URL string
*/
function encodeProductByName(config) {
const numericConfig = {
type: PRODUCT_TYPE[config.type],
color: PRODUCT_COLOR[config.color],
material: PRODUCT_MATERIAL[config.material],
hardware: []
};
if (config.hardware && config.hardware.length > 0) {
numericConfig.hardware = config.hardware.map(hw => ({
type: HARDWARE_TYPE[hw.type],
style: HARDWARE_STYLE[hw.style],
color: HARDWARE_COLOR[hw.color]
}));
}
return encodeProduct(numericConfig);
}
// ============================================================================
// DECODING FUNCTIONS
// ============================================================================
/**
* Decode a hardware hex string back to its components
*
* @param {string} hex - 4-character hex string (e.g., "0421")
* @returns {Object} Decoded hardware configuration
*/
function decodeHardware(hex) {
if (typeof hex !== 'string' || hex.length !== 4) {
throw new Error(`Invalid hardware hex string: ${hex} (must be 4 characters)`);
}
const value = parseInt(hex, 16);
if (isNaN(value)) {
throw new Error(`Invalid hex value: ${hex}`);
}
return {
type: (value >> 10) & 0x1F, // Extract bits 10-14
style: (value >> 5) & 0x1F, // Extract bits 5-9
color: value & 0x1F // Extract bits 0-4
};
}
/**
* Decode a complete product URL string
*
* @param {string} encodedString - Encoded URL string (e.g., "0-1-4-1-0421")
* @returns {Object} Decoded product configuration
*/
function decodeProduct(encodedString) {
if (typeof encodedString !== 'string') {
throw new Error('Invalid encoded string: must be a string');
}
const parts = encodedString.split('-');
if (parts.length < 4) {
throw new Error(`Invalid encoded string: ${encodedString} (must have at least 4 parts)`);
}
// Parse version
const version = parseInt(parts[0], 16);
if (version !== ENCODING_VERSION) {
throw new Error(`Unsupported encoding version: ${version} (current: ${ENCODING_VERSION})`);
}
// Parse main attributes
const config = {
version: version,
type: parseInt(parts[1], 16),
color: parseInt(parts[2], 16),
material: parseInt(parts[3], 16),
hardware: []
};
// Parse hardware items (if present)
for (let i = 4; i < parts.length; i++) {
config.hardware.push(decodeHardware(parts[i]));
}
return config;
}
/**
* Decode and return named values (convenience function)
*
* @param {string} encodedString - Encoded URL string
* @returns {Object} Decoded product with named values
*/
function decodeProductToNames(encodedString) {
const numeric = decodeProduct(encodedString);
return {
version: numeric.version,
type: PRODUCT_TYPE_REVERSE[numeric.type] || `UNKNOWN_${numeric.type}`,
color: PRODUCT_COLOR_REVERSE[numeric.color] || `UNKNOWN_${numeric.color}`,
material: PRODUCT_MATERIAL_REVERSE[numeric.material] || `UNKNOWN_${numeric.material}`,
hardware: numeric.hardware.map(hw => ({
type: HARDWARE_TYPE_REVERSE[hw.type] || `UNKNOWN_${hw.type}`,
style: HARDWARE_STYLE_REVERSE[hw.style] || `UNKNOWN_${hw.style}`,
color: HARDWARE_COLOR_REVERSE[hw.color] || `UNKNOWN_${hw.color}`
}))
};
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Get the binary representation of a hex string (for debugging)
*
* @param {string} hex - Hex string
* @returns {string} Binary string with spaces every 4 bits
*/
function hexToBinary(hex) {
const value = parseInt(hex, 16);
const binary = value.toString(2).padStart(16, '0');
return binary.match(/.{1,4}/g).join(' ');
}
/**
* Validate that a configuration uses only defined values
*
* @param {Object} config - Product configuration
* @returns {Object} Validation result with errors array
*/
function validateConfiguration(config) {
const errors = [];
if (!PRODUCT_TYPE_REVERSE[config.type]) {
errors.push(`Invalid product type: ${config.type}`);
}
if (!PRODUCT_COLOR_REVERSE[config.color]) {
errors.push(`Invalid product color: ${config.color}`);
}
if (!PRODUCT_MATERIAL_REVERSE[config.material]) {
errors.push(`Invalid product material: ${config.material}`);
}
if (config.hardware) {
config.hardware.forEach((hw, index) => {
if (!HARDWARE_TYPE_REVERSE[hw.type]) {
errors.push(`Invalid hardware ${index + 1} type: ${hw.type}`);
}
if (!HARDWARE_STYLE_REVERSE[hw.style]) {
errors.push(`Invalid hardware ${index + 1} style: ${hw.style}`);
}
if (!HARDWARE_COLOR_REVERSE[hw.color]) {
errors.push(`Invalid hardware ${index + 1} color: ${hw.color}`);
}
});
}
return {
valid: errors.length === 0,
errors: errors
};
}
// ============================================================================
// EXPORTS (for Node.js / Module usage)
// ============================================================================
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
// Constants
ENCODING_VERSION,
PRODUCT_TYPE,
PRODUCT_COLOR,
PRODUCT_MATERIAL,
HARDWARE_TYPE,
HARDWARE_STYLE,
HARDWARE_COLOR,
// Encoding functions
encodeHardware,
encodeProduct,
encodeProductByName,
// Decoding functions
decodeHardware,
decodeProduct,
decodeProductToNames,
// Utilities
hexToBinary,
validateConfiguration
};
}
// ============================================================================
// EXAMPLE USAGE
// ============================================================================
/*
// Example 1: Encode by numeric values
const encoded1 = encodeProduct({
type: 1, // Patio Door
color: 4, // White
material: 1, // Aluminum
hardware: [
{ type: 1, style: 1, color: 1 } // Lever, Standard, Brass
]
});
console.log(encoded1); // "0-1-4-1-0421"
// Example 2: Encode by named values
const encoded2 = encodeProductByName({
type: 'STORM_DOOR',
color: 'WHITE',
material: 'ALUMINUM',
hardware: [
{ type: 'LEVER', style: 'STANDARD', color: 'BRASS' },
{ type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' }
]
});
console.log(encoded2); // "0-3-4-1-0421-0C64"
// Example 3: Decode back to numeric values
const decoded1 = decodeProduct("0-1-4-1-0421");
console.log(decoded1);
// { version: 0, type: 1, color: 4, material: 1, hardware: [{ type: 1, style: 1, color: 1 }] }
// Example 4: Decode to named values
const decoded2 = decodeProductToNames("0-3-4-1-0421-0C64");
console.log(decoded2);
// {
// version: 0,
// type: 'STORM_DOOR',
// color: 'WHITE',
// material: 'ALUMINUM',
// hardware: [
// { type: 'LEVER', style: 'STANDARD', color: 'BRASS' },
// { type: 'PULL_HANDLE', style: 'ALTERNATIVE', color: 'SATIN' }
// ]
// }
// Example 5: Debug hardware encoding
const hwHex = encodeHardware(3, 12, 20);
console.log(hwHex); // "0D94"
console.log(hexToBinary(hwHex)); // "0000 1101 1001 0100"
console.log(decodeHardware(hwHex)); // { type: 3, style: 12, color: 20 }
// Example 6: Validate configuration
const validation = validateConfiguration({
type: 1, color: 4, material: 1,
hardware: [{ type: 1, style: 1, color: 1 }]
});
console.log(validation); // { valid: true, errors: [] }
*/
+1444
View File
File diff suppressed because it is too large Load Diff
+652
View File
@@ -0,0 +1,652 @@
"""
CSV to JSON Parser - Generate Navigation, Products, and Accessories JSON Files
Based on AI_PARSING_INSTRUCTIONS.md
This script reads data/products.csv and generates:
1. data/navigation.json - Question flow with conditional logic
2. data/products.json - Product catalog
3. data/accessories.json - Accessory options with compatibility rules
Run: python parse_csv_to_json.py
"""
import csv
import json
from collections import defaultdict
from typing import Dict, List, Set, Tuple
def parse_bool(value):
"""Parse TRUE/FALSE string to boolean"""
if isinstance(value, str):
return value.strip().upper() == 'TRUE'
return bool(value)
def clean_string(value):
"""Clean and strip string values"""
if isinstance(value, str):
return value.strip()
return value or ""
class CSVParser:
def __init__(self, csv_path: str):
self.csv_path = csv_path
self.products = []
self.accessories = []
self.base_types = set()
self.door_subtypes = defaultdict(list) # subtype -> [products]
self.window_subtypes = defaultdict(list)
self.material_counts = defaultdict(lambda: {'aluminum': 0, 'vinyl': 0})
self.color_counts = defaultdict(lambda: defaultdict(int)) # subtype -> {color: count}
self.color_by_material = defaultdict(lambda: defaultdict(set)) # subtype -> {material: {colors}}
self.detailed_products = {} # Load from questions.json
def load_detailed_product_info(self):
"""Load detailed product info from questions.json"""
try:
# Load the code mapping
try:
with open('data/product_code_mapping.json', 'r', encoding='utf-8') as f:
code_mapping = json.load(f)
except FileNotFoundError:
code_mapping = {}
with open('data/questions.json', 'r', encoding='utf-8') as f:
questions_data = json.load(f)
# Extract all product nodes
for key, value in questions_data.items():
if key.startswith('prod-') and value.get('type') == 'product':
code = value.get('code')
if code:
detailed_info = {
'title': value.get('title'),
'detailedDescription': value.get('description'),
'features': value.get('features', []),
'image': value.get('image'),
'url': value.get('url'),
'brochure': value.get('brochure'),
'measureGuide': value.get('measureGuide')
}
# Store under the simplified code
self.detailed_products[code] = detailed_info
# Also store under all mapped codes
for csv_code, simple_code in code_mapping.items():
if simple_code == code:
self.detailed_products[csv_code] = detailed_info
print(f"✓ Loaded {len(self.detailed_products)} detailed product descriptions")
except FileNotFoundError:
print("⚠ questions.json not found - skipping detailed product info")
except Exception as e:
print(f"⚠ Error loading product details: {e}")
def parse_csv(self):
"""Parse the CSV file and separate products from accessories"""
print("📖 Reading CSV file...")
with open(self.csv_path, 'r', encoding='utf-8', errors='ignore') as f:
reader = csv.reader(f)
# Skip first two header rows
next(reader) # Row 1: Category headers
header_row = next(reader) # Row 2: Column names
row_count = 0
for row in reader:
if len(row) < 11: # Need at least up to column K
continue
row_count += 1
# Skip discontinued items
if parse_bool(row[0]): # DISCONT column
continue
# Extract basic info
prod_code = clean_string(row[2]) # PROD_CODE
category = clean_string(row[3]) # CATEGORY
description = clean_string(row[5]) # DESCRIPTION
location = clean_string(row[1]) # LOC_CODE
base_type = clean_string(row[6]) if len(row) > 6 else "" # Base Type (G)
door_subtype = clean_string(row[7]) if len(row) > 7 else "" # Sub-type Door (H)
window_subtype = clean_string(row[8]) if len(row) > 8 else "" # Sub-type Window (I)
is_accessory = parse_bool(row[9]) if len(row) > 9 else False # Accessory Yes (J)
specific_item = parse_bool(row[10]) if len(row) > 10 else False # This Item (K)
# Parse materials (L, M)
materials = []
if len(row) > 11 and parse_bool(row[11]):
materials.append('Aluminum')
if len(row) > 12 and parse_bool(row[12]):
materials.append('Vinyl')
# Parse colors (N through S+)
colors = []
color_columns = [
(13, 'Black'), (14, 'White'), (15, 'Bronze'),
(16, 'Tan'), (17, 'Mill'), (18, 'Sandstone')
]
for col_idx, color_name in color_columns:
if len(row) > col_idx and parse_bool(row[col_idx]):
colors.append(color_name)
# Build item data
item_data = {
'id': prod_code,
'productCode': prod_code,
'category': category,
'description': description,
'discontinued': False,
'location': location,
'baseType': base_type,
'subType': {
'door': door_subtype if door_subtype else None,
'window': window_subtype if window_subtype else None
},
'materials': materials,
'colors': colors
}
# Separate accessories from products
if is_accessory:
# Build accessory-specific data
accessory = {
**item_data,
'accessoryCode': prod_code,
'compatibilityRules': self._build_compatibility_rules(
door_subtype, window_subtype, category, specific_item
),
'optionType': self._infer_option_type(description),
'metadata': {
'specificItemLink': specific_item
}
}
self.accessories.append(accessory)
else:
# Build product-specific data
product = {
**item_data,
'isAccessory': False,
'compatibleAccessories': [] # Will be populated later
}
# Merge detailed info if available
if prod_code in self.detailed_products:
detailed = self.detailed_products[prod_code]
product.update({
'title': detailed['title'],
'detailedDescription': detailed['detailedDescription'],
'features': detailed['features'],
'image': detailed['image'],
'url': detailed['url'],
'brochure': detailed.get('brochure'),
'measureGuide': detailed.get('measureGuide')
})
self.products.append(product)
# Track for navigation building
if base_type:
self.base_types.add(base_type)
# Track subtypes per type
if door_subtype:
self.door_subtypes[door_subtype].append(product)
if window_subtype:
self.window_subtypes[window_subtype].append(product)
# Count materials per subtype
subtype_key = door_subtype or window_subtype or "none"
if 'Aluminum' in materials:
self.material_counts[subtype_key]['aluminum'] += 1
# Track colors per material
for color in colors:
self.color_by_material[subtype_key]['Aluminum'].add(color)
if 'Vinyl' in materials:
self.material_counts[subtype_key]['vinyl'] += 1
# Track colors per material
for color in colors:
self.color_by_material[subtype_key]['Vinyl'].add(color)
# Count colors per subtype (overall)
for color in colors:
self.color_counts[subtype_key][color] += 1
print(f"✓ Parsed {row_count} rows")
print(f"✓ Found {len(self.products)} products")
print(f"✓ Found {len(self.accessories)} accessories")
def _build_compatibility_rules(self, door_subtype, window_subtype, category, specific_item):
"""Build compatibility rules for accessories"""
rules = {
'type': 'category', # Default
'subTypeDoor': door_subtype if door_subtype else None,
'subTypeWindow': window_subtype if window_subtype else None,
'categories': [category] if category else [],
'specificProducts': [],
'requiresMatch': {
'material': True, # Usually accessories need matching material
'color': False # Colors usually don't need to match
}
}
# Determine rule type
if specific_item:
rules['type'] = 'specific'
elif door_subtype or window_subtype:
rules['type'] = 'subType'
return rules
def _infer_option_type(self, description):
"""Infer option type from description"""
desc_upper = description.upper()
if 'HANDLE' in desc_upper:
return 'handle'
elif 'INSERT' in desc_upper or 'GLASS' in desc_upper:
return 'insert'
elif 'HARDWARE' in desc_upper:
return 'hardware'
elif 'SCREEN' in desc_upper:
return 'screen'
else:
return 'option'
def link_accessories_to_products(self):
"""Link compatible accessories to products"""
print("🔗 Linking accessories to products...")
linked_count = 0
for product in self.products:
compatible = []
for accessory in self.accessories:
rules = accessory['compatibilityRules']
# Check by subtype
if rules['type'] == 'subType':
prod_door = product['subType']['door']
prod_window = product['subType']['window']
acc_door = rules['subTypeDoor']
acc_window = rules['subTypeWindow']
if (prod_door and prod_door == acc_door) or \
(prod_window and prod_window == acc_window):
# Check material compatibility if required
if rules['requiresMatch']['material']:
# Check if they share any materials
if any(m in product['materials'] for m in accessory['materials']):
compatible.append(accessory['id'])
linked_count += 1
else:
compatible.append(accessory['id'])
linked_count += 1
# Check by category
elif rules['type'] == 'category':
if product['category'] in rules['categories']:
compatible.append(accessory['id'])
linked_count += 1
product['compatibleAccessories'] = compatible
print(f"✓ Created {linked_count} product-accessory links")
def build_navigation(self):
"""Build navigation JSON with conditional logic"""
print("🗺️ Building navigation flow...")
navigation = {}
# Question 1: Base Type Selection
navigation['start'] = {
'type': 'question',
'inputType': 'button',
'title': 'What are you looking for?',
'subtitle': 'Select the product category',
'answers': []
}
# Add base type options
sorted_types = sorted(self.base_types)
for base_type in sorted_types:
emoji = '🚪' if 'door' in base_type.lower() else '🪟'
next_key = f'q-{base_type.lower()}-type'
navigation['start']['answers'].append({
'caption': base_type,
'image': emoji,
'next': next_key,
'filter': {
'baseType': base_type
}
})
# Question 2: Sub-type Selection for Doors
if self.door_subtypes:
door_subtypes = sorted(self.door_subtypes.keys())
navigation['q-door-type'] = {
'type': 'question',
'inputType': 'button',
'title': 'What type of door?',
'subtitle': 'Select the door category',
'answers': []
}
for subtype in door_subtypes:
products = self.door_subtypes[subtype]
# Check if material question is needed
needs_material = self._needs_material_question(subtype)
# Determine next question
if needs_material:
next_key = f'q-material-{subtype.lower().replace(" ", "-")}'
else:
# No material choice needed - determine single material and check colors
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
needs_color = self._needs_color_question(subtype, single_material)
next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions'
# Auto-apply material if only one
filter_obj = {
'baseType': 'Door',
'subType': subtype
}
if not needs_material:
# Auto-apply the single material
if self.material_counts[subtype]['aluminum'] > 0:
filter_obj['material'] = 'Aluminum'
elif self.material_counts[subtype]['vinyl'] > 0:
filter_obj['material'] = 'Vinyl'
# If no color question either, auto-apply single color
single_material = filter_obj['material']
if not self._needs_color_question(subtype, single_material):
colors = list(self.color_by_material[subtype][single_material])
if len(colors) == 1:
filter_obj['color'] = colors[0]
navigation['q-door-type']['answers'].append({
'caption': subtype,
'image': '🚪',
'next': next_key,
'filter': filter_obj
})
# Create material question if needed
if needs_material:
self._create_material_question(navigation, subtype, 'Door')
# Create color question if material skipped but color needed
elif next_key.startswith('q-color-'):
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
self._create_color_question(navigation, subtype, single_material, 'Door')
# Question 2: Sub-type Selection for Windows
if self.window_subtypes:
window_subtypes = sorted(self.window_subtypes.keys())
navigation['q-window-type'] = {
'type': 'question',
'inputType': 'button',
'title': 'What type of window?',
'subtitle': 'Select the window category',
'answers': []
}
for subtype in window_subtypes:
needs_material = self._needs_material_question(subtype)
# Determine next question
if needs_material:
next_key = f'q-material-{subtype.lower().replace(" ", "-")}'
else:
# No material choice needed - determine single material and check colors
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
needs_color = self._needs_color_question(subtype, single_material)
next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions'
filter_obj = {
'baseType': 'Window',
'subType': subtype
}
if not needs_material:
if self.material_counts[subtype]['aluminum'] > 0:
filter_obj['material'] = 'Aluminum'
elif self.material_counts[subtype]['vinyl'] > 0:
filter_obj['material'] = 'Vinyl'
# If no color question either, auto-apply single color
single_material = filter_obj['material']
if not self._needs_color_question(subtype, single_material):
colors = list(self.color_by_material[subtype][single_material])
if len(colors) == 1:
filter_obj['color'] = colors[0]
navigation['q-window-type']['answers'].append({
'caption': subtype,
'image': '🪟',
'next': next_key,
'filter': filter_obj
})
if needs_material:
self._create_material_question(navigation, subtype, 'Window')
# Create color question if material skipped but color needed
elif next_key.startswith('q-color-'):
single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl'
self._create_color_question(navigation, subtype, single_material, 'Window')
# Final Question: Dimensions
navigation['q-dimensions'] = {
'type': 'question',
'inputType': 'form',
'title': 'Enter Product Dimensions',
'subtitle': 'Please provide the measurements',
'fields': [
{
'name': 'width',
'label': 'Width (inches)',
'type': 'number',
'required': True,
'placeholder': 'e.g., 36'
},
{
'name': 'height',
'label': 'Height (inches)',
'type': 'number',
'required': True,
'placeholder': 'e.g., 80'
}
],
'next': 'results'
}
print(f"✓ Created {len(navigation)} navigation nodes")
return navigation
def _needs_material_question(self, subtype):
"""Check if a subtype needs a material selection question"""
aluminum = self.material_counts[subtype]['aluminum']
vinyl = self.material_counts[subtype]['vinyl']
# Show material question if both materials exist
return aluminum > 0 and vinyl > 0
def _create_material_question(self, navigation, subtype, base_type):
"""Create a material selection question"""
question_key = f'q-material-{subtype.lower().replace(" ", "-")}'
navigation[question_key] = {
'type': 'question',
'inputType': 'button',
'title': 'Select Material',
'subtitle': f'Choose your preferred material for {subtype}',
'conditional': {
'type': 'material',
'fallbackNext': 'q-dimensions'
},
'answers': []
}
# Add material options
if self.material_counts[subtype]['aluminum'] > 0:
# Check if color question needed after aluminum selection
needs_color = self._needs_color_question(subtype, 'Aluminum')
next_key_al = f'q-color-{subtype.lower().replace(" ", "-")}-aluminum' if needs_color else 'q-dimensions'
navigation[question_key]['answers'].append({
'caption': 'Aluminum',
'image': '🔩',
'next': next_key_al,
'filter': {
'baseType': base_type,
'subType': subtype,
'material': 'Aluminum'
}
})
if self.material_counts[subtype]['vinyl'] > 0:
# Check if color question needed after vinyl selection
needs_color = self._needs_color_question(subtype, 'Vinyl')
next_key_vin = f'q-color-{subtype.lower().replace(" ", "-")}-vinyl' if needs_color else 'q-dimensions'
navigation[question_key]['answers'].append({
'caption': 'Vinyl',
'image': '🪟',
'next': next_key_vin,
'filter': {
'baseType': base_type,
'subType': subtype,
'material': 'Vinyl'
}
})
# Create color questions for each material option
if self.material_counts[subtype]['aluminum'] > 0 and self._needs_color_question(subtype, 'Aluminum'):
self._create_color_question(navigation, subtype, 'Aluminum', base_type)
if self.material_counts[subtype]['vinyl'] > 0 and self._needs_color_question(subtype, 'Vinyl'):
self._create_color_question(navigation, subtype, 'Vinyl', base_type)
def _needs_color_question(self, subtype, material):
"""Check if a subtype+material combination needs a color selection question"""
colors = self.color_by_material[subtype][material]
# Show color question if multiple colors exist
return len(colors) > 1
def _create_color_question(self, navigation, subtype, material, base_type):
"""Create a color selection question"""
question_key = f'q-color-{subtype.lower().replace(" ", "-")}-{material.lower()}'
navigation[question_key] = {
'type': 'question',
'inputType': 'button',
'title': 'Select Color',
'subtitle': f'Choose your preferred color for {material} {subtype}',
'conditional': {
'type': 'color',
'fallbackNext': 'q-dimensions'
},
'answers': []
}
# Get available colors for this subtype+material
colors = sorted(self.color_by_material[subtype][material])
# Color emoji mapping
color_emojis = {
'Black': '',
'White': '',
'Bronze': '🟫',
'Tan': '🟤',
'Mill': '',
'Sandstone': '🟨'
}
for color in colors:
navigation[question_key]['answers'].append({
'caption': color,
'image': color_emojis.get(color, '🎨'),
'next': 'q-dimensions',
'filter': {
'baseType': base_type,
'subType': subtype,
'material': material,
'color': color
}
})
def save_json_files(self, navigation):
"""Save all three JSON files"""
print("💾 Saving JSON files...")
# Save products.json
with open('data/products.json', 'w', encoding='utf-8') as f:
json.dump(self.products, f, indent=2)
print(f"✓ Saved data/products.json ({len(self.products)} products)")
# Save accessories.json
with open('data/accessories.json', 'w', encoding='utf-8') as f:
json.dump(self.accessories, f, indent=2)
print(f"✓ Saved data/accessories.json ({len(self.accessories)} accessories)")
# Save navigation.json
with open('data/navigation.json', 'w', encoding='utf-8') as f:
json.dump(navigation, f, indent=2)
print(f"✓ Saved data/navigation.json ({len(navigation)} nodes)")
def print_statistics(self):
"""Print parsing statistics"""
print("\n" + "="*60)
print("PARSING STATISTICS")
print("="*60)
print(f"Total Products: {len(self.products)}")
print(f"Total Accessories: {len(self.accessories)}")
print(f"Base Types: {', '.join(sorted(self.base_types))}")
print(f"Door Subtypes: {len(self.door_subtypes)}")
print(f"Window Subtypes: {len(self.window_subtypes)}")
print()
print("Material Distribution:")
for subtype, counts in sorted(self.material_counts.items()):
if counts['aluminum'] > 0 or counts['vinyl'] > 0:
needs_q = "✓ Material Q" if (counts['aluminum'] > 0 and counts['vinyl'] > 0) else "✗ Skip"
print(f" {subtype:20} - Al:{counts['aluminum']:3} Vinyl:{counts['vinyl']:3} [{needs_q}]")
print("="*60)
def main():
csv_path = 'data/products.csv'
print("🚀 CSV to JSON Parser")
print("="*60)
try:
parser = CSVParser(csv_path)
parser.load_detailed_product_info()
parser.parse_csv()
parser.link_accessories_to_products()
navigation = parser.build_navigation()
parser.save_json_files(navigation)
parser.print_statistics()
print("\n✅ All JSON files generated successfully!")
print("\nNext steps:")
print("1. Review data/navigation.json for question flow")
print("2. Review data/products.json for product data")
print("3. Review data/accessories.json for accessory compatibility")
print("4. Test the application in your browser")
except FileNotFoundError:
print(f"❌ Error: Could not find {csv_path}")
print(" Make sure products.csv exists in the data/ folder")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()
+47
View File
@@ -0,0 +1,47 @@
"""
Passenger WSGI file for Flask application
Compatible with Python 3.13+
Auto-detects /product-finder subdirectory deployment
"""
import sys
import os
# Get the directory where this file is located
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
# Add the application directory to Python path
sys.path.insert(0, CURRENT_DIR)
# Import the Flask application directly from app.py
# Note: app.py now auto-detects production environment
try:
from app import app as application
print("✓ Flask application loaded successfully")
except ImportError as e:
# Fallback error handler that shows the actual error
def application(environ, start_response):
import traceback
status = '500 Internal Server Error'
error_details = f'Import Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}\n\nPython Path:\n{sys.path}'
output = error_details.encode('utf-8')
response_headers = [('Content-type', 'text/plain; charset=utf-8'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
print(f"✗ Failed to import Flask app: {e}")
except Exception as e:
# Catch any other startup errors
def application(environ, start_response):
import traceback
status = '500 Internal Server Error'
error_details = f'Startup Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}'
output = error_details.encode('utf-8')
response_headers = [('Content-type', 'text/plain; charset=utf-8'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
print(f"✗ Error starting app: {e}")
# Passenger requires the application object to be named 'application'
+189
View File
@@ -0,0 +1,189 @@
"""
Interactive Region Coordinate Helper
Opens an image and helps you find pixel coordinates for colorable regions.
Click on the image to see coordinates.
Usage:
python region_helper.py images/cobrai.jpg
"""
import sys
from PIL import Image, ImageDraw, ImageFont
import tkinter as tk
from tkinter import Canvas
from PIL import ImageTk
class RegionHelper:
def __init__(self, image_path):
self.image_path = image_path
self.image = Image.open(image_path)
self.width, self.height = self.image.size
# Setup UI
self.root = tk.Tk()
self.root.title(f"Region Helper - {image_path}")
# Scale image if too large
self.scale = 1.0
max_display_width = 1000
max_display_height = 800
if self.width > max_display_width or self.height > max_display_height:
scale_w = max_display_width / self.width
scale_h = max_display_height / self.height
self.scale = min(scale_w, scale_h)
new_width = int(self.width * self.scale)
new_height = int(self.height * self.scale)
self.display_image = self.image.resize((new_width, new_height), Image.LANCZOS)
else:
self.display_image = self.image.copy()
# Setup canvas
self.canvas = Canvas(
self.root,
width=self.display_image.width,
height=self.display_image.height
)
self.canvas.pack(side=tk.LEFT)
# Display image
self.photo = ImageTk.PhotoImage(self.display_image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
# Info panel
info_frame = tk.Frame(self.root, width=300)
info_frame.pack(side=tk.RIGHT, fill=tk.BOTH, padx=10, pady=10)
tk.Label(info_frame, text="Image Dimensions:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=5)
tk.Label(info_frame, text=f"{self.width} x {self.height} pixels").pack(anchor=tk.W)
if self.scale != 1.0:
tk.Label(info_frame, text=f"Scaled: {self.scale:.2%}").pack(anchor=tk.W)
tk.Label(info_frame, text="\nClick to get coordinates:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=(20, 5))
self.coord_label = tk.Label(info_frame, text="X: -, Y: -", font=('Arial', 14))
self.coord_label.pack(anchor=tk.W, pady=5)
tk.Label(info_frame, text="\nFirst click = Top-Left", fg='blue').pack(anchor=tk.W)
tk.Label(info_frame, text="Second click = Bottom-Right", fg='blue').pack(anchor=tk.W)
tk.Label(info_frame, text="\nRegion JSON:", font=('Arial', 12, 'bold')).pack(anchor=tk.W, pady=(20, 5))
self.json_text = tk.Text(info_frame, height=15, width=35, font=('Courier', 9))
self.json_text.pack(fill=tk.BOTH, expand=True)
tk.Button(info_frame, text="Clear Points", command=self.clear_points).pack(pady=10)
tk.Button(info_frame, text="Copy JSON", command=self.copy_json).pack()
# Bind events
self.canvas.bind('<Motion>', self.on_mouse_move)
self.canvas.bind('<Button-1>', self.on_click)
# Store points
self.points = []
self.point_ids = []
self.update_json()
def on_mouse_move(self, event):
"""Show current mouse coordinates."""
x = int(event.x / self.scale)
y = int(event.y / self.scale)
self.coord_label.config(text=f"X: {x}, Y: {y}")
def on_click(self, event):
"""Record clicked point."""
x = int(event.x / self.scale)
y = int(event.y / self.scale)
# Limit to 2 points (top-left, bottom-right)
if len(self.points) >= 2:
self.clear_points()
self.points.append((x, y))
# Draw point on canvas
color = 'blue' if len(self.points) == 1 else 'red'
point_id = self.canvas.create_oval(
event.x - 5, event.y - 5,
event.x + 5, event.y + 5,
fill=color, outline='white', width=2
)
self.point_ids.append(point_id)
# Draw rectangle if we have both points
if len(self.points) == 2:
x1, y1 = self.points[0]
x2, y2 = self.points[1]
# Draw on canvas (scaled)
rect_id = self.canvas.create_rectangle(
x1 * self.scale, y1 * self.scale,
x2 * self.scale, y2 * self.scale,
outline='green', width=2, dash=(5, 5)
)
self.point_ids.append(rect_id)
self.update_json()
def clear_points(self):
"""Clear all points and rectangles."""
for point_id in self.point_ids:
self.canvas.delete(point_id)
self.points = []
self.point_ids = []
self.update_json()
def update_json(self):
"""Update the JSON display."""
self.json_text.delete('1.0', tk.END)
if len(self.points) == 0:
self.json_text.insert('1.0', '{\n "name": "region_name",\n "topLeft": [?, ?],\n "bottomRight": [?, ?],\n "description": "..."\n}')
elif len(self.points) == 1:
x, y = self.points[0]
self.json_text.insert('1.0', f'{{\n "name": "region_name",\n "topLeft": [{x}, {y}],\n "bottomRight": [?, ?],\n "description": "..."\n}}')
else:
x1, y1 = self.points[0]
x2, y2 = self.points[1]
# Calculate dimensions
width = abs(x2 - x1)
height = abs(y2 - y1)
json_str = f'''{{\n "name": "region_name",\n "topLeft": [{x1}, {y1}],\n "bottomRight": [{x2}, {y2}],\n "description": "Width: {width}px, Height: {height}px"\n}}'''
self.json_text.insert('1.0', json_str)
def copy_json(self):
"""Copy JSON to clipboard."""
json_str = self.json_text.get('1.0', tk.END).strip()
self.root.clipboard_clear()
self.root.clipboard_append(json_str)
self.root.update()
def run(self):
"""Start the GUI."""
self.root.mainloop()
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python region_helper.py <image_path>")
print("Example: python region_helper.py images/cobrai.jpg")
sys.exit(1)
image_path = sys.argv[1]
try:
helper = RegionHelper(image_path)
helper.run()
except FileNotFoundError:
print(f"Error: Image file not found: {image_path}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
+4
View File
@@ -0,0 +1,4 @@
Flask>=3.0.0
Werkzeug>=3.0.0
# Pillow>=10.0.0 # Optional - only needed for dynamic image generation
# If you need image generation, install separately: pip install Pillow
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page Not Found</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.error-container {
text-align: center;
background-color: white;
padding: 60px 40px;
border: 2px solid #333;
border-radius: 8px;
max-width: 600px;
}
h1 {
font-size: 120px;
color: #333;
margin-bottom: 20px;
}
h2 {
font-size: 32px;
color: #333;
margin-bottom: 15px;
}
p {
font-size: 16px;
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
.button {
display: inline-block;
padding: 12px 30px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 16px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #555;
}
</style>
</head>
<body>
<div class="error-container">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>Sorry, the page you're looking for doesn't exist or has been moved.</p>
<a href="/" class="button">Go Home</a>
<a href="/quiz" class="button" style="margin-left: 10px;">Start Quiz</a>
</div>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Denied - CGW Product Finder</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.access-denied-container {
background: white;
padding: 50px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 500px;
}
.icon {
font-size: 80px;
margin-bottom: 20px;
}
h1 {
color: #f56565;
margin-bottom: 15px;
font-size: 32px;
}
.message {
color: #666;
margin-bottom: 10px;
font-size: 16px;
line-height: 1.6;
}
.permission-info {
background: #fed7d7;
color: #742a2a;
padding: 12px;
border-radius: 5px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
border-left: 4px solid #f56565;
}
.btn {
display: inline-block;
padding: 12px 30px;
margin: 10px 5px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
transform: translateY(-2px);
}
.contact-info {
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid #e0e0e0;
color: #999;
font-size: 14px;
}
</style>
</head>
<body>
<div class="access-denied-container">
<div class="icon">🚫</div>
<h1>Access Denied</h1>
<p class="message">
You do not have permission to access this page or perform this action.
</p>
{% if required_permission %}
<div class="permission-info">
Required Permission: <strong>{{ required_permission }}</strong>
</div>
{% endif %}
<p class="message">
If you believe this is an error, please contact your administrator.
</p>
<div style="margin-top: 30px;">
<a href="javascript:history.back()" class="btn btn-secondary">← Go Back</a>
<a href="{{ url_for('index') }}" class="btn btn-primary">Return to Home</a>
</div>
<div class="contact-info">
Need help? Contact your system administrator to request access.
</div>
</div>
</body>
</html>
+381
View File
@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Image Generation - Test Page</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.demo-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 40px;
}
.image-container {
border: 2px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #fafafa;
text-align: center;
}
.image-container h3 {
margin-bottom: 15px;
color: #333;
}
.product-image {
max-width: 100%;
height: auto;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
max-height: 500px;
object-fit: contain;
}
.loading {
color: #999;
font-style: italic;
padding: 40px;
}
.controls {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.control-group select,
.control-group input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.radio-group {
display: flex;
gap: 15px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 5px;
font-weight: normal;
}
button {
background: #333;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
button:hover {
background: #555;
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196F3;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.info-box h4 {
color: #1976D2;
margin-bottom: 5px;
}
.info-box p {
color: #555;
line-height: 1.6;
}
.url-display {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
word-break: break-all;
margin-top: 10px;
}
.error {
background: #ffebee;
border-left: 4px solid #f44336;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
color: #c62828;
}
@media (max-width: 768px) {
.demo-section {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎨 Dynamic Image Generation API</h1>
<p class="subtitle">Test the product image generation endpoints</p>
<div class="info-box">
<h4>How It Works</h4>
<p>
This demo uses server-side image processing to dynamically generate product images
with different colors and configurations. Select options below to see the image update in real-time.
</p>
</div>
<div class="controls">
<h3>Product Configuration</h3>
<div class="control-group">
<label for="product-select">Product:</label>
<select id="product-select">
<option value="cobrai">COBRAI - Storm Door</option>
</select>
</div>
<div class="control-group">
<label for="color-select">Color:</label>
<select id="color-select">
<option value="white">White</option>
<option value="black">Black</option>
<option value="bronze">Bronze</option>
<option value="sandstone">Sandstone</option>
</select>
</div>
<div class="control-group">
<label>Hinge Location:</label>
<div class="radio-group">
<label>
<input type="radio" name="hinge" value="right" checked>
Right Hinge
</label>
<label>
<input type="radio" name="hinge" value="left">
Left Hinge
</label>
</div>
</div>
<div class="control-group">
<label>
<input type="checkbox" id="cache-checkbox" checked>
Use Cache
</label>
</div>
<button onclick="updateImages()">🔄 Update Images</button>
<button onclick="clearCache()">🗑️ Clear Cache</button>
<button onclick="getConfig()">📋 Get Config</button>
</div>
<div class="demo-section">
<div class="image-container">
<h3>Direct Image URL Method</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
Using: <code>&lt;img src="..."&gt;</code>
</p>
<img id="direct-image" class="product-image" src="" alt="Product">
<div class="url-display" id="direct-url">Loading...</div>
</div>
<div class="image-container">
<h3>JSON/Base64 Method</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
Using: <code>fetch() + base64</code>
</p>
<img id="json-image" class="product-image" src="" alt="Product">
<div class="url-display" id="json-url">Loading...</div>
</div>
</div>
<div id="error-box" style="display: none;" class="error"></div>
<div id="config-box" style="display: none; margin-top: 20px;">
<h3>Product Configuration</h3>
<pre id="config-display" style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto;"></pre>
</div>
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd;">
<h3>API Endpoints Used</h3>
<ul style="line-height: 2;">
<li><code>GET /api/product-image/{product}?color={color}&hinge={hinge}</code></li>
<li><code>GET /api/product-image/{product}?color={color}&hinge={hinge}&format=json</code></li>
<li><code>GET /api/product-config/{product}</code></li>
<li><code>POST /api/clear-image-cache</code></li>
</ul>
</div>
</div>
<script>
// Initialize on load
window.addEventListener('DOMContentLoaded', () => {
updateImages();
});
// Auto-update when controls change
document.getElementById('product-select').addEventListener('change', updateImages);
document.getElementById('color-select').addEventListener('change', updateImages);
document.querySelectorAll('[name="hinge"]').forEach(radio => {
radio.addEventListener('change', updateImages);
});
function updateImages() {
const product = document.getElementById('product-select').value;
const color = document.getElementById('color-select').value;
const hinge = document.querySelector('[name="hinge"]:checked').value;
const useCache = document.getElementById('cache-checkbox').checked;
hideError();
// Method 1: Direct image URL
updateDirectImage(product, color, hinge, useCache);
// Method 2: JSON with base64
updateJsonImage(product, color, hinge, useCache);
}
function updateDirectImage(product, color, hinge, useCache) {
const url = `/api/product-image/${product}?color=${color}&hinge=${hinge}&cache=${useCache}`;
const img = document.getElementById('direct-image');
img.src = url;
document.getElementById('direct-url').textContent = url;
}
async function updateJsonImage(product, color, hinge, useCache) {
const url = `/api/product-image/${product}?color=${color}&hinge=${hinge}&format=json&cache=${useCache}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === 'success') {
document.getElementById('json-image').src = data.image;
document.getElementById('json-url').textContent = url;
} else {
showError(`Error: ${data.error || 'Unknown error'}`);
}
} catch (error) {
showError(`Fetch error: ${error.message}`);
}
}
async function getConfig() {
const product = document.getElementById('product-select').value;
const url = `/api/product-config/${product}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === 'success') {
document.getElementById('config-display').textContent =
JSON.stringify(data.config, null, 2);
document.getElementById('config-box').style.display = 'block';
} else {
showError(`Error: ${data.error || 'Unknown error'}`);
}
} catch (error) {
showError(`Fetch error: ${error.message}`);
}
}
async function clearCache() {
const product = document.getElementById('product-select').value;
try {
const response = await fetch('/api/clear-image-cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ productCode: product })
});
const data = await response.json();
if (data.status === 'success') {
alert(data.message);
updateImages();
} else {
showError(`Error: ${data.error || 'Unknown error'}`);
}
} catch (error) {
showError(`Fetch error: ${error.message}`);
}
}
function showError(message) {
const errorBox = document.getElementById('error-box');
errorBox.textContent = message;
errorBox.style.display = 'block';
}
function hideError() {
document.getElementById('error-box').style.display = 'none';
}
</script>
</body>
</html>
+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Details Form</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 20px;
border: 2px solid #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
input[type="text"],
input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
}
textarea {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
button {
width: 100%;
padding: 12px;
background-color: #d0d0ff;
border: 1px solid #333;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-bottom: 15px;
}
button:hover {
background-color: #b0b0ff;
}
.specifications-section {
border: 3px solid #4CAF50;
padding: 15px;
margin-bottom: 15px;
background-color: #f0fff0;
}
.specifications-section.hidden {
display: none;
}
.conditional-field {
display: none;
}
.conditional-field.visible {
display: block;
}
select {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
}
.spec-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.spec-row label {
margin-bottom: 0;
}
.spec-row input {
border: 1px solid #333;
padding: 6px;
}
.questions-section {
border: 1px solid #333;
padding: 15px;
text-align: center;
background-color: #fafafa;
}
.questions-section p {
margin-bottom: 10px;
font-size: 12px;
}
.questions-section .placeholder {
font-style: italic;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="form-group">
<label>Type (door, window, other)</label>
<select id="typeSelect" onchange="toggleOtherField()">
<option value="">-- Select Type --</option>
<option value="door">Door</option>
<option value="window">Window</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group conditional-field" id="otherField">
<label>Please specify</label>
<input type="text" placeholder="Enter type details">
</div>
<div class="form-group">
<label>Description</label>
<textarea placeholder="textfield"></textarea>
</div>
<div class="form-group">
<label>Notes</label>
<textarea placeholder="textfield"></textarea>
</div>
<button onclick="toggleSpecifications()">Match and Review</button>
<div class="specifications-section hidden" id="specificationsSection">
<div class="spec-row">
<label>Product Code:</label>
<input type="text" placeholder="" readonly>
</div>
<div class="spec-row">
<label>Width:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Height</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Color</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Frame:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Screen:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Etc...</label>
<input type="text" placeholder="">
</div>
</div>
<div class="form-group">
<label>Quantity</label>
<input type="number" min="1" value="1" placeholder="">
</div>
<button>Confirm and Add</button>
<div class="questions-section">
<p>Possible questions for customers based on what is selected or missing</p>
<div class="placeholder">[list - checkbox per question]</div>
</div>
</div>
<script>
function toggleSpecifications() {
const section = document.getElementById('specificationsSection');
section.classList.toggle('hidden');
}
function toggleOtherField() {
const typeSelect = document.getElementById('typeSelect');
const otherField = document.getElementById('otherField');
if (typeSelect.value === 'other') {
otherField.classList.add('visible');
} else {
otherField.classList.remove('visible');
}
}
</script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Finder</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>Product Finder</h1>
<div class="user-info-bar" id="userInfoBar">
<span id="userLocationInfo"></span>
<button onclick="logout()" class="logout-btn">Logout</button>
</div>
</div>
<div class="breadcrumb" id="breadcrumb">
Start
</div>
<div id="content">
<!-- Content will be dynamically loaded here -->
</div>
</div>
<script>
// Base URL for API calls (supports subdirectory deployments)
const BASE_URL = '{{ base_url }}';
// Load user session info
async function loadUserInfo() {
try {
const response = await fetch(BASE_URL + '/api/session');
const data = await response.json();
if (data.status === 'success') {
const locationNames = {
'LINDS': 'Lindsborg',
'IOLA': 'Iola',
'KC': 'KC',
'BMD': 'BMD'
};
const currentLoc = locationNames[data.currentLocation] || data.currentLocation;
document.getElementById('userLocationInfo').textContent =
`👤 ${data.username} | 📍 ${currentLoc}`;
}
} catch (error) {
console.error('Error loading user info:', error);
}
}
async function logout() {
if (confirm('Are you sure you want to log out?')) {
try {
await fetch(BASE_URL + '/api/logout', { method: 'POST' });
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Successfully logged out');
} catch (error) {
window.location.href = BASE_URL + '/login';
}
}
}
// Load user info when page loads
loadUserInfo();
</script>
<script>
// Pass BASE_URL to script.js by setting it as a window variable
window.APP_BASE_URL = BASE_URL;
</script>
<script src="{{ url_for('serve_js', filename='script.js') }}"></script>
</body>
</html>
+255
View File
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - CGW Product Finder</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
background: #667eea;
color: white;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
.alert {
padding: 12px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background: #fed7d7;
color: #742a2a;
border-left: 4px solid #f56565;
}
.alert-info {
background: #bee3f8;
color: #2c5282;
border-left: 4px solid #4299e1;
}
.footer-links {
text-align: center;
margin-top: 20px;
font-size: 14px;
}
.footer-links a {
color: #667eea;
text-decoration: none;
}
.footer-links a:hover {
text-decoration: underline;
}
.loading-spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 10px;
vertical-align: middle;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>🔐 CGW Product Finder</h1>
<p>Please sign in to continue</p>
</div>
<div class="alert alert-error" id="errorAlert"></div>
<form id="loginForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus
placeholder="Enter your username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required
placeholder="Enter your password">
</div>
<button type="submit" class="btn" id="loginBtn">
Sign In
<span class="loading-spinner" id="loadingSpinner"></span>
</button>
</form>
<div class="footer-links">
<a href="{{ url_for('user_manager') }}">User Management</a>
</div>
</div>
<script>
// Base URL for API calls (supports subdirectory deployments)
const BASE_URL = '{{ base_url }}';
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
const spinner = document.getElementById('loadingSpinner');
// Disable button and show spinner
loginBtn.disabled = true;
spinner.style.display = 'inline-block';
try {
const response = await fetch(BASE_URL + '/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.status === 'success') {
// Check if user needs to select a location
if (data.requiresLocationSelection) {
// Redirect to location selection page
window.location.href = '/select-location';
} else {
// Redirect to main app
window.location.href = '/';
}
} else {
showAlert(data.message || 'Invalid username or password');
loginBtn.disabled = false;
spinner.style.display = 'none';
}
} catch (error) {
showAlert('Error connecting to server. Please try again.');
loginBtn.disabled = false;
spinner.style.display = 'none';
}
});
function showAlert(message) {
const alert = document.getElementById('errorAlert');
alert.textContent = message;
alert.classList.add('show');
setTimeout(() => {
alert.classList.remove('show');
}, 5000);
}
// Check if there's a message in URL (e.g., after logout)
const urlParams = new URLSearchParams(window.location.search);
const message = urlParams.get('message');
if (message) {
showAlert(message);
}
</script>
</body>
</html>
+359
View File
@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select Location - CGW Product Finder</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.location-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 500px;
}
.location-header {
text-align: center;
margin-bottom: 30px;
}
.location-header h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.location-header p {
color: #666;
font-size: 14px;
}
.user-info {
background: #f7fafc;
padding: 15px;
border-radius: 5px;
margin-bottom: 25px;
border-left: 4px solid #667eea;
}
.user-info p {
color: #333;
margin-bottom: 5px;
}
.user-info strong {
color: #667eea;
}
.location-list {
margin-bottom: 25px;
}
.location-item {
background: #f7fafc;
padding: 15px 20px;
border-radius: 5px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
display: flex;
align-items: center;
}
.location-item:hover {
background: #e6f2ff;
border-color: #667eea;
transform: translateX(5px);
}
.location-item input[type="radio"] {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.location-item label {
cursor: pointer;
flex: 1;
font-size: 16px;
color: #333;
font-weight: 500;
}
.location-badge {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
background: #667eea;
color: white;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
.btn-logout {
background: #f56565;
margin-top: 15px;
}
.btn-logout:hover {
background: #e53e3e;
box-shadow: 0 5px 15px rgba(245, 101, 101, 0.4);
}
.btn-admin {
background: #48bb78;
margin-top: 15px;
}
.btn-admin:hover {
background: #38a169;
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.4);
}
.btn-admin.hidden {
display: none;
}
.alert {
padding: 12px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background: #fed7d7;
color: #742a2a;
border-left: 4px solid #f56565;
}
</style>
</head>
<body>
<div class="location-container">
<div class="location-header">
<h1>📍 Select Your Location</h1>
<p>Choose a location to access the Product Finder</p>
</div>
<div class="user-info" id="userInfo">
<p><strong>Welcome:</strong> <span id="userName"></span></p>
<p><strong>Default Location:</strong> <span id="defaultLocation"></span></p>
</div>
<div class="alert alert-error" id="errorAlert"></div>
<form id="locationForm">
<div class="location-list" id="locationList">
<!-- Locations will be populated here -->
</div>
<button type="submit" class="btn" id="continueBtn">
Continue to Product Finder
</button>
</form>
<button class="btn btn-admin hidden" id="userManagementBtn" onclick="goToUserManagement()">
⚙️ User Management
</button>
<button class="btn btn-logout" onclick="logout()">
Sign Out
</button>
</div>
<script>
// Base URL for API calls (supports subdirectory deployments)
const BASE_URL = '{{ base_url }}';
const LOCATION_NAMES = {
'LINDS': 'Lindsborg',
'IOLA': 'Iola',
'KC': 'KC',
'BMD': 'BMD'
};
let sessionData = null;
async function loadLocationData() {
try {
const response = await fetch(BASE_URL + '/api/session');
const data = await response.json();
if (data.status === 'success') {
sessionData = data;
// Populate user info
document.getElementById('userName').textContent = data.username;
document.getElementById('defaultLocation').textContent =
LOCATION_NAMES[data.defaultLocation] || data.defaultLocation;
// Populate location list
const locationList = document.getElementById('locationList');
const locations = data.accessibleLocations || [];
if (locations.length === 0) {
showAlert('No accessible locations found. Please contact an administrator.');
document.getElementById('continueBtn').disabled = true;
return;
}
locationList.innerHTML = locations.map((code, index) => {
const isDefault = code === data.defaultLocation;
const checked = isDefault ? 'checked' : '';
return `
<div class="location-item" onclick="selectLocation('${code}')">
<input type="radio" name="location" value="${code}"
id="loc_${code}" ${checked}>
<label for="loc_${code}">
${LOCATION_NAMES[code] || code}
</label>
${isDefault ? '<span class="location-badge">Default</span>' : ''}
</div>
`;
}).join('');
} else {
// Not logged in or session expired
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Please log in first');
}
// Check if user has manage_users permission
await checkUserManagementPermission();
} catch (error) {
showAlert('Error loading location data. Please try again.');
}
}
async function checkUserManagementPermission() {
try {
const response = await fetch(BASE_URL + '/api/check-permission', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ permission: 'manage_users' })
});
const data = await response.json();
if (data.status === 'success' && data.hasPermission) {
document.getElementById('userManagementBtn').classList.remove('hidden');
}
} catch (error) {
// Permission check failed, keep button hidden
console.error('Error checking permission:', error);
}
}
function goToUserManagement() {
window.location.href = BASE_URL + '/users';
}
function selectLocation(code) {
document.getElementById(`loc_${code}`).checked = true;
}
document.getElementById('locationForm').addEventListener('submit', async (e) => {
e.preventDefault();
const selectedLocation = document.querySelector('input[name="location"]:checked')?.value;
if (!selectedLocation) {
showAlert('Please select a location');
return;
}
try {
const response = await fetch(BASE_URL + '/api/select-location', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ location: selectedLocation })
});
const data = await response.json();
if (data.status === 'success') {
// Redirect to main app
window.location.href = BASE_URL + '/';
} else {
showAlert(data.message || 'Error selecting location');
}
} catch (error) {
showAlert('Error connecting to server. Please try again.');
}
});
async function logout() {
try {
await fetch(BASE_URL + '/api/logout', { method: 'POST' });
window.location.href = BASE_URL + '/login?message=' + encodeURIComponent('Successfully logged out');
} catch (error) {
window.location.href = BASE_URL + '/login';
}
}
function showAlert(message) {
const alert = document.getElementById('errorAlert');
alert.textContent = message;
alert.classList.add('show');
setTimeout(() => {
alert.classList.remove('show');
}, 5000);
}
// Load location data on page load
loadLocationData();
</script>
</body>
</html>
+976
View File
@@ -0,0 +1,976 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Manager - CGW Product Finder</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
margin-bottom: 30px;
text-align: center;
}
.header h1 {
color: #333;
margin-bottom: 10px;
}
.header p {
color: #666;
}
.card {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
margin-bottom: 30px;
}
.card h2 {
color: #333;
margin-bottom: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.location-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.location-table th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
border: 1px solid #5568d3;
}
.location-table th.checkbox-header {
text-align: center;
width: 100px;
cursor: pointer;
user-select: none;
}
.location-table th.checkbox-header:hover {
background: #5568d3;
}
.location-table td {
padding: 12px;
border: 1px solid #e0e0e0;
background: white;
}
.location-table td.checkbox-cell {
text-align: center;
}
.location-table tr:hover td {
background: #f7fafc;
}
.location-table input[type="checkbox"],
.location-table input[type="radio"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.location-table label {
cursor: pointer;
display: flex;
align-items: center;
}
.location-name {
font-weight: 500;
color: #333;
}
.tooltip-header {
position: relative;
display: inline-block;
cursor: help;
}
.tooltip-header .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
font-size: 13px;
font-weight: normal;
}
.tooltip-header .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.tooltip-header:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover {
background: #38a169;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.4);
}
.btn-danger {
background: #f56565;
color: white;
margin-left: 10px;
}
.btn-danger:hover {
background: #e53e3e;
}
.btn-warning {
background: #ed8936;
color: white;
}
.btn-warning:hover {
background: #dd6b20;
}
.user-list {
margin-top: 20px;
}
.user-item {
background: #f7fafc;
padding: 15px;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid #667eea;
}
.user-item.inactive {
opacity: 0.6;
border-left-color: #cbd5e0;
}
.user-actions {
display: flex;
align-items: center;
gap: 15px;
}
.active-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.active-toggle input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.active-label {
font-weight: 500;
}
.active-label.active {
color: #48bb78;
}
.active-label.inactive {
color: #f56565;
}
.user-info {
flex: 1;
}
.user-info strong {
color: #333;
display: block;
margin-bottom: 5px;
}
.user-info span {
color: #666;
font-size: 14px;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.alert.show {
display: block;
}
.alert-success {
background: #c6f6d5;
color: #22543d;
border-left: 4px solid #48bb78;
}
.alert-error {
background: #fed7d7;
color: #742a2a;
border-left: 4px solid #f56565;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.stats {
display: flex;
justify-content: space-around;
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #e0e0e0;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
font-size: 14px;
}
.back-link {
display: inline-block;
margin-top: 20px;
color: white;
text-decoration: none;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
transition: background 0.3s;
}
.back-link:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
justify-content: center;
align-items: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 500px;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e0e0e0;
}
.modal-header h2 {
margin: 0;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 28px;
color: #999;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #f56565;
}
.modal-body {
margin-bottom: 20px;
}
.security-note {
background: #fef5e7;
border-left: 4px solid #ed8936;
padding: 12px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 14px;
color: #744210;
}
.security-note strong {
display: block;
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 User Manager</h1>
<p>Create and manage user accounts with secure password encoding</p>
</div>
<div class="alert alert-success" id="successAlert"></div>
<div class="alert alert-error" id="errorAlert"></div>
<div class="card">
<h2>Add New User</h2>
<form id="userForm">
<div class="form-group">
<label for="username">Username *</label>
<input type="text" id="username" name="username" required
placeholder="Enter username (e.g., john_doe)">
</div>
<div class="form-group">
<label for="password">Password *</label>
<input type="password" id="password" name="password" required
placeholder="Enter a secure password" minlength="6">
</div>
<div class="form-group">
<label>Location Configuration</label>
<table class="location-table">
<thead>
<tr>
<th>Default Location *</th>
<th class="checkbox-header">
<label class="tooltip-header" style="display: flex; align-items: center; justify-content: center; cursor: pointer; margin: 0;">
<input type="checkbox" id="headerAccessible" onchange="toggleAllCheckboxes('accessible')" style="margin: 0;">
<span class="tooltip-text">Can access location</span>
</label>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<label>
<input type="radio" name="defaultLocation" value="LINDS" required style="margin-right: 8px;">
<span class="location-name">Lindsborg</span>
</label>
</td>
<td class="checkbox-cell">
<input type="checkbox" class="accessible-checkbox" data-location="LINDS" onchange="updateHeaderCheckbox('accessible')">
</td>
</tr>
<tr>
<td>
<label>
<input type="radio" name="defaultLocation" value="IOLA" required style="margin-right: 8px;">
<span class="location-name">Iola</span>
</label>
</td>
<td class="checkbox-cell">
<input type="checkbox" class="accessible-checkbox" data-location="IOLA" onchange="updateHeaderCheckbox('accessible')">
</td>
</tr>
<tr>
<td>
<label>
<input type="radio" name="defaultLocation" value="KC" required style="margin-right: 8px;">
<span class="location-name">KC</span>
</label>
</td>
<td class="checkbox-cell">
<input type="checkbox" class="accessible-checkbox" data-location="KC" onchange="updateHeaderCheckbox('accessible')">
</td>
</tr>
<tr>
<td>
<label>
<input type="radio" name="defaultLocation" value="BMD" required style="margin-right: 8px;">
<span class="location-name">BMD</span>
</label>
</td>
<td class="checkbox-cell">
<input type="checkbox" class="accessible-checkbox" data-location="BMD" onchange="updateHeaderCheckbox('accessible')">
</td>
</tr>
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<div class="stats">
<div class="stat-item">
<div class="stat-number" id="userCount">0</div>
<div class="stat-label">Total Users</div>
</div>
</div>
</div>
<div class="card">
<h2>User List</h2>
<div class="user-list" id="userList">
<p style="color: #666; text-align: center;">No users added yet.</p>
</div>
<div class="button-group">
<button class="btn btn-success" id="downloadBtn" onclick="downloadUsers()">
📥 Download Users JSON
</button>
<button class="btn btn-danger" id="clearBtn" onclick="clearAllUsers()">
🗑️ Clear All Users
</button>
</div>
</div>
<a href="{{ url_for('index') }}" class="back-link">← Back to Product Finder</a>
</div>
<!-- Change Password Modal -->
<div id="changePasswordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>🔑 Change Password</h2>
<button class="close-btn" onclick="closePasswordModal()">&times;</button>
</div>
<div class="modal-body">
<div class="security-note">
<strong>🔒 Security Verification Required</strong>
You must enter your current password to change another user's password.
</div>
<form id="changePasswordForm">
<div class="form-group">
<label>User to Update</label>
<input type="text" id="targetUsername" readonly style="background: #f0f0f0;">
</div>
<div class="form-group">
<label for="newPassword">New Password for User *</label>
<input type="password" id="newPassword" required
placeholder="Enter new password for this user">
</div>
<div class="form-group">
<label for="confirmPassword">Confirm New Password *</label>
<input type="password" id="confirmPassword" required
placeholder="Re-enter new password">
</div>
<div class="form-group">
<label for="currentUserPassword">Your Password (Current User) *</label>
<input type="password" id="currentUserPassword" required
placeholder="Enter YOUR password to verify">
</div>
<div class="button-group">
<button type="submit" class="btn btn-primary">
Change Password
</button>
<button type="button" class="btn btn-danger" onclick="closePasswordModal()">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Base URL for API calls (supports subdirectory deployments)
const BASE_URL = '{{ base_url }}';
let users = [];
const LOCATIONS = [
{ name: 'Lindsborg', code: 'LINDS' },
{ name: 'Iola', code: 'IOLA' },
{ name: 'KC', code: 'KC' },
{ name: 'BMD', code: 'BMD' }
];
// Toggle all checkboxes in a column
function toggleAllCheckboxes(type) {
const headerCheckbox = document.getElementById(`header${type.charAt(0).toUpperCase() + type.slice(1)}`);
const checkboxes = document.querySelectorAll(`.${type}-checkbox`);
// Get the state AFTER the checkbox was clicked (it's already toggled by the browser)
const newState = headerCheckbox.checked;
// Apply this state to all row checkboxes
checkboxes.forEach(cb => cb.checked = newState);
// Clear indeterminate state since we're setting all to the same value
headerCheckbox.indeterminate = false;
}
// Update header checkbox state based on individual checkboxes
function updateHeaderCheckbox(type) {
const headerCheckbox = document.getElementById(`header${type.charAt(0).toUpperCase() + type.slice(1)}`);
const checkboxes = document.querySelectorAll(`.${type}-checkbox`);
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
if (checkedCount === 0) {
headerCheckbox.checked = false;
headerCheckbox.indeterminate = false;
} else if (checkedCount === checkboxes.length) {
headerCheckbox.checked = true;
headerCheckbox.indeterminate = false;
} else {
headerCheckbox.checked = false;
headerCheckbox.indeterminate = true;
}
}
// Load existing users on page load
async function loadUsers() {
try {
const response = await fetch(BASE_URL + '/api/users');
const data = await response.json();
if (data.status === 'success') {
users = data.users || [];
renderUsers();
}
} catch (error) {
console.error('Error loading users:', error);
}
}
// Render users in the list
function renderUsers() {
const userList = document.getElementById('userList');
const userCount = document.getElementById('userCount');
userCount.textContent = users.length;
if (users.length === 0) {
userList.innerHTML = '<p style="color: #666; text-align: center;">No users added yet.</p>';
return;
}
userList.innerHTML = users.map((user, index) => {
// Build location info string
let locationInfo = '';
if (user.locationSettings) {
const defaultLoc = LOCATIONS.find(l => l.code === user.defaultLocation);
const defaultName = defaultLoc ? defaultLoc.name : user.defaultLocation;
const accessible = Object.entries(user.locationSettings)
.filter(([code, settings]) => settings.accessible)
.map(([code]) => LOCATIONS.find(l => l.code === code)?.name || code);
locationInfo = `Default: ${defaultName}`;
if (accessible.length > 0) locationInfo += ` | Access: ${accessible.join(', ')}`;
} else if (user.mainLocation) {
// Backwards compatibility
const mainLoc = LOCATIONS.find(l => l.code === user.mainLocation);
locationInfo = `Main: ${mainLoc ? mainLoc.name : user.mainLocation}`;
if (user.alternateLocations && user.alternateLocations.length > 0) {
const altNames = user.alternateLocations.map(code =>
LOCATIONS.find(l => l.code === code)?.name || code
);
locationInfo += ` | Alt: ${altNames.join(', ')}`;
}
} else if (user.location) {
// Old format
locationInfo = user.location;
}
const isActive = user.active !== false; // Default to active if not specified
const activeClass = isActive ? '' : ' inactive';
const activeStatus = isActive ? 'active' : 'inactive';
const activeLabel = isActive ? 'Active' : 'Inactive';
return `
<div class="user-item${activeClass}">
<div class="user-info">
<strong>👤 ${user.username}</strong>
<span>📍 ${locationInfo}</span>
</div>
<div class="user-actions">
<div class="active-toggle">
<input type="checkbox" ${isActive ? 'checked' : ''}
onchange="toggleUserActive(${index})"
id="userActive${index}">
<label for="userActive${index}" class="active-label ${activeStatus}">${activeLabel}</label>
</div>
<button class="btn btn-warning" onclick="openPasswordModal(${index})" style="margin: 0 5px; padding: 8px 16px;">
Change Password
</button>
<button class="btn btn-danger" onclick="deleteUser(${index})" style="margin: 0; padding: 8px 16px;">
Delete
</button>
</div>
</div>
`;
}).join('');
}
// Handle form submission
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
// Get default location (radio button)
const defaultLocation = document.querySelector('input[name="defaultLocation"]:checked')?.value;
// Get location settings
const locationSettings = {};
LOCATIONS.forEach(loc => {
const accessibleCheckbox = document.querySelector(`.accessible-checkbox[data-location="${loc.code}"]`);
locationSettings[loc.code] = {
accessible: accessibleCheckbox ? accessibleCheckbox.checked : false
};
});
const formData = {
username: document.getElementById('username').value,
password: document.getElementById('password').value,
defaultLocation: defaultLocation,
locationSettings: locationSettings,
active: true // New users are active by default
};
try {
const response = await fetch(BASE_URL + '/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.status === 'success') {
showAlert('success', `User "${formData.username}" added successfully!`);
users = data.users;
renderUsers();
document.getElementById('userForm').reset();
// Reset header checkboxes
document.getElementById('headerAccessible').checked = false;
document.getElementById('headerAccessible').indeterminate = false;
} else {
showAlert('error', data.message || 'Error adding user');
}
} catch (error) {
showAlert('error', 'Error connecting to server: ' + error.message);
}
});
// Toggle user active status
async function toggleUserActive(index) {
try {
const isActive = document.getElementById(`userActive${index}`).checked;
const response = await fetch(BASE_URL + `/api/users/${index}/active`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ active: isActive })
});
const data = await response.json();
if (data.status === 'success') {
users = data.users;
renderUsers();
} else {
showAlert('error', data.message || 'Error updating user status');
// Revert checkbox on error
document.getElementById(`userActive${index}`).checked = !isActive;
}
} catch (error) {
showAlert('error', 'Error connecting to server: ' + error.message);
// Revert checkbox on error
const checkbox = document.getElementById(`userActive${index}`);
checkbox.checked = !checkbox.checked;
}
}
// Delete a specific user
async function deleteUser(index) {
if (!confirm('Are you sure you want to delete this user?')) {
return;
}
try {
const response = await fetch(BASE_URL + `/api/users/${index}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.status === 'success') {
showAlert('success', 'User deleted successfully!');
users = data.users;
renderUsers();
} else {
showAlert('error', data.message || 'Error deleting user');
}
} catch (error) {
showAlert('error', 'Error connecting to server: ' + error.message);
}
}
// Clear all users
async function clearAllUsers() {
if (!confirm('Are you sure you want to delete ALL users? This cannot be undone!')) {
return;
}
try {
const response = await fetch(BASE_URL + '/api/users/clear', {
method: 'DELETE'
});
const data = await response.json();
if (data.status === 'success') {
showAlert('success', 'All users cleared successfully!');
users = [];
renderUsers();
} else {
showAlert('error', data.message || 'Error clearing users');
}
} catch (error) {
showAlert('error', 'Error connecting to server: ' + error.message);
}
}
// Download users as JSON
function downloadUsers() {
if (users.length === 0) {
showAlert('error', 'No users to download!');
return;
}
window.location.href = BASE_URL + '/api/users/download';
showAlert('success', 'Downloading users.json...');
}
// Password change modal functions
let targetUserIndex = null;
function openPasswordModal(userIndex) {
targetUserIndex = userIndex;
const user = users[userIndex];
document.getElementById('targetUsername').value = user.username;
document.getElementById('changePasswordModal').classList.add('show');
document.getElementById('changePasswordForm').reset();
document.getElementById('targetUsername').value = user.username;
}
function closePasswordModal() {
document.getElementById('changePasswordModal').classList.remove('show');
document.getElementById('changePasswordForm').reset();
targetUserIndex = null;
}
// Handle password change form submission
document.getElementById('changePasswordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const currentUserPassword = document.getElementById('currentUserPassword').value;
// Validate passwords match
if (newPassword !== confirmPassword) {
showAlert('error', 'New passwords do not match!');
return;
}
// Validate password length
if (newPassword.length < 6) {
showAlert('error', 'Password must be at least 6 characters long!');
return;
}
try {
const response = await fetch(BASE_URL + `/api/users/${targetUserIndex}/change-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
newPassword: newPassword,
currentUserPassword: currentUserPassword
})
});
const data = await response.json();
if (data.status === 'success') {
showAlert('success', data.message);
closePasswordModal();
} else {
showAlert('error', data.message || 'Error changing password');
}
} catch (error) {
showAlert('error', 'Error connecting to server. Please try again.');
}
});
// Close modal when clicking outside of it
window.onclick = function(event) {
const modal = document.getElementById('changePasswordModal');
if (event.target === modal) {
closePasswordModal();
}
};
// Show alert messages
function showAlert(type, message) {
const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
const alert = document.getElementById(alertId);
alert.textContent = message;
alert.classList.add('show');
setTimeout(() => {
alert.classList.remove('show');
}, 5000);
}
// Load users when page loads
loadUsers();
</script>
</body>
</html>
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""
Comprehensive app test script
Run this through control panel to diagnose issues
Writes detailed logs to test_app.log
"""
import sys
import os
from datetime import datetime
# Setup logging
log_file = os.path.join(os.path.dirname(__file__), 'test_app.log')
def log(message, also_print=True):
"""Write to log file and optionally print"""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
full_message = f"[{timestamp}] {message}"
with open(log_file, 'a') as f:
f.write(full_message + '\n')
if also_print:
print(full_message)
try:
log("="*70)
log("APP TEST SCRIPT - START")
log("="*70)
# 1. Environment Info
log("\n1. PYTHON ENVIRONMENT")
log(f" Python: {sys.version}")
log(f" Executable: {sys.executable}")
log(f" CWD: {os.getcwd()}")
# 2. Check environment variables
log("\n2. ENVIRONMENT VARIABLES")
log(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
log(f" FLASK_ENV: {os.environ.get('FLASK_ENV', 'NOT SET')}")
log(f" SECRET_KEY: {'SET' if os.environ.get('SECRET_KEY') else 'NOT SET'}")
# 3. Check critical files
log("\n3. FILE STRUCTURE CHECK")
files_to_check = [
'app.py',
'passenger_wsgi.py',
'config.py',
'data/users.json',
'templates/login.html',
'templates/index2.html',
'templates/select_location.html',
'templates/user_manager.html'
]
all_files_exist = True
for file_path in files_to_check:
full_path = os.path.join(os.path.dirname(__file__), file_path)
exists = os.path.exists(full_path)
status = "" if exists else "✗ MISSING"
log(f" {status} {file_path}")
if not exists:
all_files_exist = False
if not all_files_exist:
log("\n⚠️ CRITICAL: Missing files detected!")
# 4. Check dependencies
log("\n4. PYTHON PACKAGES")
packages = [
('flask', 'Flask'),
('werkzeug', 'Werkzeug')
]
all_packages_ok = True
for module_name, display_name in packages:
try:
module = __import__(module_name)
try:
from importlib.metadata import version
ver = version(module_name)
except:
ver = getattr(module, '__version__', 'unknown')
log(f"{display_name}: {ver}")
except ImportError as e:
log(f"{display_name}: NOT INSTALLED ({e})")
all_packages_ok = False
if not all_packages_ok:
log("\n⚠️ CRITICAL: Missing packages! Run: pip install Flask==3.0.0 Werkzeug==3.0.1")
sys.exit(1)
# 5. Try importing app
log("\n5. APP IMPORT TEST")
log(" Attempting to import Flask app...")
try:
# Make sure current directory is in path
current_dir = os.path.dirname(__file__)
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from app import app
log(" ✓ Flask app imported successfully!")
# Check app config
log("\n6. APP CONFIGURATION")
log(f" APPLICATION_ROOT: {app.config.get('APPLICATION_ROOT', 'NOT SET')}")
log(f" DEBUG: {app.config.get('DEBUG')}")
log(f" SECRET_KEY: {'SET (' + str(len(app.config.get('SECRET_KEY', ''))) + ' chars)' if app.config.get('SECRET_KEY') else 'NOT SET'}")
# Check if middleware applied
log(f" WSGI App Type: {type(app.wsgi_app).__name__}")
if 'PrefixMiddleware' in str(type(app.wsgi_app)):
log(" ✓ PrefixMiddleware is active")
else:
log(" ⚠️ PrefixMiddleware NOT active (may cause issues at /product-finder)")
# Count routes
log("\n7. ROUTES CHECK")
routes = list(app.url_map.iter_rules())
log(f" Total routes: {len(routes)}")
# Check critical routes
critical_routes = ['/login', '/api/login', '/users', '/select-location']
log(" Critical routes:")
for route_path in critical_routes:
found = any(str(rule) == route_path or str(rule).startswith(route_path + '<') for rule in routes)
status = "" if found else "✗ MISSING"
log(f" {status} {route_path}")
# 8. Test loading users
log("\n8. USER DATA TEST")
try:
users_file = os.path.join(current_dir, 'data', 'users.json')
if os.path.exists(users_file):
import json
with open(users_file, 'r') as f:
users = json.load(f)
log(f" ✓ Users file loaded: {len(users)} users")
log(f" Usernames: {', '.join([u.get('username', '?') for u in users])}")
else:
log(f" ✗ Users file NOT FOUND at {users_file}")
except Exception as e:
log(f" ✗ Error loading users: {e}")
# 9. Test a simple request
log("\n9. REQUEST TEST")
log(" Testing if app can handle requests...")
with app.test_client() as client:
# Test root redirect
try:
response = client.get('/')
log(f" GET / -> Status: {response.status_code}")
if response.status_code == 302:
log(f" Redirects to: {response.location}")
except Exception as e:
log(f" ✗ Error testing /: {e}")
# Test login page
try:
response = client.get('/login')
log(f" GET /login -> Status: {response.status_code}")
if response.status_code != 200:
log(f" ⚠️ Login page returned {response.status_code} instead of 200")
except Exception as e:
log(f" ✗ Error testing /login: {e}")
# 10. Summary
log("\n" + "="*70)
log("TEST SUMMARY")
log("="*70)
if all_files_exist and all_packages_ok:
log("✓ All files present")
log("✓ All packages installed")
log("✓ App imports successfully")
log(f"{len(routes)} routes registered")
app_root = app.config.get('APPLICATION_ROOT', '/')
if app_root == '/product-finder':
log("✓ Configured for /product-finder deployment")
else:
log(f"⚠️ APPLICATION_ROOT is '{app_root}' (should be '/product-finder' for production)")
log("\n🎉 APP APPEARS READY!")
log("\nIf still getting 500 errors:")
log("1. Check that data/users.json exists")
log("2. Verify APPLICATION_ROOT env var is set to /product-finder")
log("3. Check Apache error log for runtime errors")
log("4. Ensure .htaccess is correct")
else:
log("\n⚠️ ISSUES FOUND - Fix these before deployment")
log("="*70)
log(f"\nLog saved to: {log_file}")
except ImportError as e:
log(f" ✗ FAILED to import app!")
log(f" Error: {e}")
import traceback
log("\nFull traceback:")
log(traceback.format_exc())
log("\n❌ CRITICAL ERROR: Cannot import Flask app")
log("Check that app.py exists and has no syntax errors")
except Exception as e:
log(f" ✗ Unexpected error during import!")
log(f" Error: {e}")
import traceback
log("\nFull traceback:")
log(traceback.format_exc())
except Exception as e:
log(f"\n❌ FATAL ERROR: {e}")
import traceback
log(traceback.format_exc())
finally:
log("\n" + "="*70)
log("TEST COMPLETE")
log("="*70)
print(f"\n📄 Full log saved to: {log_file}")
print("Download this file to see all test results")
+171
View File
@@ -0,0 +1,171 @@
"""
Deployment diagnostic script for /product-finder
Run this on the server to verify configuration
"""
import os
import sys
print("=" * 70)
print("CGW PRODUCT FINDER - DEPLOYMENT DIAGNOSTIC")
print("=" * 70)
print()
# Check Python version
print("📌 Python Environment:")
print(f" Version: {sys.version}")
print(f" Executable: {sys.executable}")
print(f" Current Directory: {os.getcwd()}")
print()
# Check environment variable
print("📌 Environment Variables:")
app_root_env = os.environ.get('APPLICATION_ROOT')
if app_root_env:
print(f" APPLICATION_ROOT: {app_root_env}")
else:
print(f" APPLICATION_ROOT: NOT SET ⚠️")
print(f" Setting temporarily for test: /product-finder")
os.environ['APPLICATION_ROOT'] = '/product-finder'
print()
# Try importing Flask
print("📌 Flask Installation:")
try:
import flask
print(f" Flask version: {flask.__version__}")
except ImportError as e:
print(f" ✗ Flask not installed: {e}")
sys.exit(1)
# Try importing app
print()
print("📌 Application Import:")
try:
from app import app
print(f" ✓ App imported successfully")
except Exception as e:
print(f" ✗ Error importing app:")
print(f" {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Check app configuration
print()
print("📌 Application Configuration:")
print(f" APPLICATION_ROOT: {app.config.get('APPLICATION_ROOT')}")
print(f" SECRET_KEY set: {'Yes' if app.config.get('SECRET_KEY') else 'No'}")
print(f" DEBUG mode: {app.config.get('DEBUG')}")
print(f" ENV: {app.config.get('ENV', 'not set')}")
print()
# Check middleware
print("📌 WSGI Middleware:")
wsgi_app_type = type(app.wsgi_app).__name__
if 'PrefixMiddleware' in str(type(app.wsgi_app)):
print(f" ✓ PrefixMiddleware applied")
print(f" Type: {wsgi_app_type}")
elif wsgi_app_type == 'Flask':
print(f" ⚠️ No middleware applied (running at root)")
print(f" Type: {wsgi_app_type}")
else:
print(f" Middleware type: {wsgi_app_type}")
print()
# Check routes
print("📌 Registered Routes:")
routes_count = len(list(app.url_map.iter_rules()))
print(f" Total routes: {routes_count}")
# Check critical routes
critical_routes = ['/login', '/api/login', '/users', '/api/session']
print(f" Critical routes:")
for route in critical_routes:
found = any(str(rule) == route for rule in app.url_map.iter_rules())
status = "" if found else ""
print(f" {status} {route}")
print()
# Check file structure
print("📌 File Structure:")
critical_files = [
'app.py',
'passenger_wsgi.py',
'config.py',
'templates/login.html',
'templates/index2.html',
'data/users.json'
]
for file_path in critical_files:
exists = os.path.exists(file_path)
status = "" if exists else ""
print(f" {status} {file_path}")
print()
# Check dependencies
print("📌 Python Dependencies:")
deps = [
('werkzeug', 'Werkzeug'),
('flask', 'Flask'),
]
try:
from PIL import Image
deps.append(('PIL', 'Pillow'))
except ImportError:
pass
for module_name, display_name in deps:
try:
module = __import__(module_name)
version = getattr(module, '__version__', 'unknown')
print(f"{display_name}: {version}")
except ImportError:
print(f"{display_name}: NOT INSTALLED")
print()
# Summary
print("=" * 70)
print("DEPLOYMENT STATUS")
print("=" * 70)
issues = []
if not os.environ.get('APPLICATION_ROOT'):
issues.append("APPLICATION_ROOT not set in environment")
if app.config.get('APPLICATION_ROOT') == '/':
issues.append("APPLICATION_ROOT is '/' but should be '/product-finder' for production")
if not os.path.exists('templates/login.html'):
issues.append("Template files missing")
if not os.path.exists('data/users.json'):
issues.append("User data file missing")
if issues:
print()
print("⚠️ ISSUES FOUND:")
for issue in issues:
print(f" - {issue}")
print()
print("📝 NEXT STEPS:")
print(" 1. Upload missing files to server")
print(" 2. Set APPLICATION_ROOT environment variable")
print(" 3. Restart Passenger: touch tmp/restart.txt")
print(" 4. Check error logs for details")
else:
print()
print("✓ All checks passed!")
print()
print("🚀 DEPLOYMENT READY")
print()
print("To restart Passenger:")
print(" mkdir -p tmp && touch tmp/restart.txt")
print()
print("To test:")
print(" https://columbiawindows.com/product-finder/")
print()
print("=" * 70)
+86
View File
@@ -0,0 +1,86 @@
"""
Quick test to verify Flask routes are registered correctly
Run this from the app folder: python test_routes.py
"""
import sys
import os
# Add current directory to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from app import app
print("=" * 60)
print("FLASK ROUTES TEST")
print("=" * 60)
print()
print("✓ Flask app imported successfully")
print()
print("Registered routes:")
print("-" * 60)
routes = []
for rule in app.url_map.iter_rules():
routes.append({
'endpoint': rule.endpoint,
'methods': ', '.join(sorted(rule.methods - {'HEAD', 'OPTIONS'})),
'path': str(rule)
})
# Sort by path
routes.sort(key=lambda x: x['path'])
# Find all login-related routes
login_routes = [r for r in routes if 'login' in r['path'].lower() or 'login' in r['endpoint'].lower()]
user_routes = [r for r in routes if 'user' in r['path'].lower()]
print("\n📝 Login & Authentication Routes:")
for route in login_routes:
print(f" {route['path']:40} [{route['methods']:15}] -> {route['endpoint']}")
print("\n👥 User Management Routes:")
for route in user_routes:
print(f" {route['path']:40} [{route['methods']:15}] -> {route['endpoint']}")
print("\n📊 Total routes registered:", len(routes))
print()
print("=" * 60)
print("✓ All routes loaded successfully!")
print("=" * 60)
print()
print("To start the server:")
print(" python app.py")
print()
print("Then access:")
print(" http://localhost:8080/login")
print(" http://localhost:8080/users")
print()
except ImportError as e:
print("=" * 60)
print("❌ IMPORT ERROR")
print("=" * 60)
print()
print(f"Failed to import Flask app: {e}")
print()
print("This might be because:")
print(" 1. Flask is not installed: pip install Flask")
print(" 2. Werkzeug is not installed: pip install Werkzeug")
print(" 3. There's a syntax error in app.py")
print()
print("Try running:")
print(" pip install -r requirements.txt")
print()
except Exception as e:
print("=" * 60)
print("❌ ERROR")
print("=" * 60)
print()
print(f"Error: {e}")
print()
import traceback
traceback.print_exc()
print()
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Server troubleshooting script for 403/404 errors
Run this on the server to diagnose Apache/Passenger issues
"""
import os
import sys
print("=" * 70)
print("TROUBLESHOOTING 403/404 ERRORS")
print("=" * 70)
print()
# Check current directory
current_dir = os.getcwd()
print(f"📁 Current directory: {current_dir}")
print()
# Check file permissions
print("📌 File Permissions Check:")
files_to_check = [
'passenger_wsgi.py',
'.htaccess',
'app.py',
'data/users.json',
'templates/login.html'
]
for file in files_to_check:
if os.path.exists(file):
stat_info = os.stat(file)
perms = oct(stat_info.st_mode)[-3:]
print(f"{file:<30} Permissions: {perms}")
else:
print(f"{file:<30} NOT FOUND")
print()
# Check directory permissions
print("📌 Directory Permissions:")
dirs_to_check = ['.', 'templates', 'data', 'static', 'tmp']
for dir_path in dirs_to_check:
if os.path.exists(dir_path):
stat_info = os.stat(dir_path)
perms = oct(stat_info.st_mode)[-3:]
print(f"{dir_path:<30} Permissions: {perms}")
else:
print(f" ⚠️ {dir_path:<30} NOT FOUND (might be OK)")
print()
# Check for tmp/restart.txt
print("📌 Passenger Restart Check:")
if os.path.exists('tmp/restart.txt'):
from datetime import datetime
mtime = os.path.getmtime('tmp/restart.txt')
mtime_str = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
print(f" ✓ tmp/restart.txt exists (last modified: {mtime_str})")
else:
print(f" ✗ tmp/restart.txt NOT FOUND")
print(f" Create it with: mkdir -p tmp && touch tmp/restart.txt")
print()
# Check environment variables
print("📌 Environment Variables:")
print(f" APPLICATION_ROOT: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
print(f" HOME: {os.environ.get('HOME', 'NOT SET')}")
print(f" USER: {os.environ.get('USER', 'NOT SET')}")
print()
# Check .htaccess content
print("📌 .htaccess Configuration:")
if os.path.exists('.htaccess'):
print(f" ✓ .htaccess exists")
with open('.htaccess', 'r') as f:
content = f.read()
if 'PassengerEnabled' in content:
print(f" ✓ PassengerEnabled directive found")
else:
print(f" ✗ PassengerEnabled directive NOT FOUND")
if 'PassengerAppRoot' in content:
print(f" ✓ PassengerAppRoot directive found")
# Extract the path
for line in content.split('\n'):
if 'PassengerAppRoot' in line:
print(f" Value: {line.strip()}")
else:
print(f" ✗ PassengerAppRoot NOT FOUND")
if 'PassengerPython' in content:
print(f" ✓ PassengerPython directive found")
for line in content.split('\n'):
if 'PassengerPython' in line:
print(f" Value: {line.strip()}")
python_path = line.split()[-1] if len(line.split()) > 1 else ''
if os.path.exists(python_path):
print(f" ✓ Python executable exists")
else:
print(f" ✗ Python executable NOT FOUND at {python_path}")
else:
print(f" ✗ .htaccess NOT FOUND")
print()
# Try importing Flask
print("📌 Flask Import Test:")
try:
sys.path.insert(0, current_dir)
from app import app
print(f" ✓ Flask app imported successfully")
print(f" Routes registered: {len(list(app.url_map.iter_rules()))}")
except Exception as e:
print(f" ✗ Error importing app: {e}")
print()
# Recommendations
print("=" * 70)
print("COMMON FIXES FOR 403/404 ERRORS:")
print("=" * 70)
print()
print("1️⃣ RESTART PASSENGER:")
print(" mkdir -p tmp")
print(" touch tmp/restart.txt")
print()
print("2️⃣ CHECK FILE PERMISSIONS:")
print(" chmod 644 passenger_wsgi.py")
print(" chmod 644 .htaccess")
print(" chmod 755 .")
print()
print("3️⃣ VERIFY PATHS IN .htaccess:")
print(" PassengerAppRoot should point to: " + current_dir)
print()
print("4️⃣ CHECK APACHE ERROR LOG:")
print(" tail -50 ~/logs/error_log")
print(" (Look for Passenger errors)")
print()
print("5️⃣ VERIFY .htaccess IS BEING READ:")
print(" If AllowOverride is not set in Apache config, .htaccess is ignored")
print()
print("6️⃣ SIMPLER .htaccess (if still failing):")
print(" Try creating a minimal .htaccess with just:")
print(" PassengerEnabled on")
print()
print("=" * 70)
+8
View File
@@ -0,0 +1,8 @@
"""
WSGI entry point for production deployment
Use with Gunicorn: gunicorn wsgi:app
"""
from app import app
if __name__ == "__main__":
app.run()
+42
View File
@@ -0,0 +1,42 @@
# Simple Flask Test App
A minimal 2-page Flask application to test Passenger WSGI deployment.
## Pages
- `/` - Home page (Hello World)
- `/about` - About page
- `/test` - Test page (shows system info)
## Local Testing
```bash
cd app_simple
python app.py
```
Visit: http://localhost:8080/
## Server Deployment
### Upload these files to: `/home/bmdwtjuw/product-finder/`
- `app.py`
- `wsgi.py`
- `templates/home.html`
- `templates/about.html`
### Control Panel Configuration
- **Application startup file:** `wsgi.py`
- **Application Entry point:** `app`
- **Python version:** 3.13.11
### Test URLs (after deployment)
- https://columbiawindows.com/product-finder/
- https://columbiawindows.com/product-finder/about
- https://columbiawindows.com/product-finder/test
## What This Tests
✓ Python 3.13.11 compatibility
✓ Flask framework loading
✓ Template rendering
✓ URL routing
✓ Passenger WSGI configuration
Once this works, you can add back the login/user management features from `app2` folder.
+37
View File
@@ -0,0 +1,37 @@
from flask import Flask, render_template
# Initialize Flask
app = Flask(__name__)
@app.route('/')
def home():
"""Home page - Hello World"""
return render_template('home.html')
@app.route('/about')
def about():
"""About page"""
return render_template('about.html')
@app.route('/test')
def test():
"""Test route - no template needed"""
import sys
return f"""
<html>
<head><title>Test Page</title></head>
<body style="font-family: Arial; padding: 20px;">
<h1>Flask App is Running!</h1>
<ul>
<li><strong>Python:</strong> {sys.version}</li>
<li><strong>Flask:</strong> Working</li>
<li><strong>Routes:</strong> {len(list(app.url_map.iter_rules()))}</li>
</ul>
<p><a href="/">Home</a> | <a href="/about">About</a></p>
</body>
</html>
"""
if __name__ == '__main__':
# Run locally on port 8080
app.run(debug=True, host='0.0.0.0', port=8080)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Deployment Check Script
Run this on the server to verify configuration
"""
import os
import sys
print("=" * 70)
print("DEPLOYMENT CONFIGURATION CHECK")
print("=" * 70)
# Check Python version
print(f"\nPython Version: {sys.version}")
print(f"Python Executable: {sys.executable}")
# Check current directory
print(f"\nCurrent Directory: {os.getcwd()}")
# Check for required files
print("\n" + "=" * 70)
print("CHECKING FILES")
print("=" * 70)
files_to_check = ['app.py', 'wsgi.py', 'templates/home.html', 'templates/about.html']
for file in files_to_check:
exists = "✓ EXISTS" if os.path.exists(file) else "✗ MISSING"
print(f"{exists}: {file}")
# Check for passenger_wsgi.py (should NOT exist)
if os.path.exists('passenger_wsgi.py'):
print("✗ WARNING: passenger_wsgi.py exists - DELETE THIS FILE!")
else:
print("✓ GOOD: passenger_wsgi.py does not exist")
# Try importing Flask
print("\n" + "=" * 70)
print("CHECKING FLASK")
print("=" * 70)
try:
import flask
print(f"✓ Flask version: {flask.__version__}")
except ImportError as e:
print(f"✗ Flask not installed: {e}")
# Try importing the app
print("\n" + "=" * 70)
print("CHECKING APP IMPORT")
print("=" * 70)
try:
from app import app
print("✓ App imported successfully")
print(f"✓ App name: {app.name}")
print(f"✓ Routes count: {len(list(app.url_map.iter_rules()))}")
except Exception as e:
print(f"✗ Error importing app: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 70)
print("DONE")
print("=" * 70)
+2
View File
@@ -0,0 +1,2 @@
Flask==3.0.0
Werkzeug==3.0.1
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - Simple Flask App</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
padding: 40px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
margin: 0 0 20px 0;
font-size: 2.5em;
}
a {
color: #ffd700;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.nav {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<div class="container">
<h1>About This App</h1>
<p>This is a simple 2-page Flask application created to test server deployment configuration.</p>
<p><strong>Purpose:</strong> Verify that Passenger WSGI configuration works correctly.</p>
<h2>Configuration:</h2>
<ul>
<li><strong>Startup file:</strong> wsgi.py</li>
<li><strong>Entry point:</strong> app</li>
<li><strong>Framework:</strong> Flask 3.0.0</li>
<li><strong>Python:</strong> 3.13.11</li>
</ul>
<div class="nav">
<a href="/">Home</a> |
<a href="/about">About</a> |
<a href="/test">Test</a>
</div>
</div>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home - Simple Flask App</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
padding: 40px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
margin: 0 0 20px 0;
font-size: 2.5em;
}
a {
color: #ffd700;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.nav {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<div class="container">
<h1>Hello World!</h1>
<p>Welcome to the simple Flask application.</p>
<p>This is a minimal test to verify the server configuration works correctly.</p>
<div class="nav">
<a href="/">Home</a> |
<a href="/about">About</a> |
<a href="/test">Test</a>
</div>
</div>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
from app import app
if __name__ == "__main__":
app.run()
+723
View File
@@ -0,0 +1,723 @@
# AI Data Parsing Instructions
## 📋 Quick Reference
**Input**: `data/products.csv` (1000 rows with product and accessory data)
**Outputs**:
1. `data/navigation.json` - Smart navigation flow with conditional questions
2. `data/products.json` - Product catalog with materials, colors, and accessory links
3. `data/accessories.json` - Accessory options with compatibility rules
**Key Features**:
- ✅ Conditional material questions (only show if multiple materials exist in sub-type)
- ✅ Conditional color questions (only show if multiple colors available)
- ✅ Auto-skip questions when only one option exists
- ✅ Smart product-accessory linking by sub-type, category, or specific product code
- 🔄 Multi-group assignment using bit values (optional enhancement)
---
## Overview
Parse the `data/products.csv` file to generate three separate JSON files that structure product data, navigation, and accessory options for a doors and windows e-commerce application.
## Source File Structure
### CSV File: `data/products.csv`
The CSV contains product and accessory data with the following column structure:
**Row 1**: Category headers (informational)
**Row 2**: Column names (actual headers)
### Column Mapping (0-indexed):
- **A (0)**: DISCONT - Discontinued flag (TRUE/FALSE)
- **B (1)**: LOC_CODE - Location code (e.g., "Iola")
- **C (2)**: PROD_CODE - Product code (unique identifier)
- **D (3)**: CATEGORY - Category code
- **E (4)**: CATEGORY_NEW - New category (usually empty)
- **F (5)**: DESCRIPTION - Full product description
- **G (6)**: Base Type - Primary type ("Window", "Door", or empty)
- **H (7)**: Sub-type Door - Door subcategory (e.g., "Storm Door")
- **I (8)**: Sub-type Window - Window subcategory (e.g., "Storm Window")
- **J (9)**: Accessory Yes - Boolean indicating if item is an accessory
- **K (10)**: This Item - Boolean for specific item relationship
- **L+ (11+)**: Material and Color availability (Aluminum, Vinyl, Black, White, Bronze, Tan, Mill, Sandstone, etc.)
---
## Output File 1: `data/navigation.json`
### Purpose
Generate a navigation flow with questions and button options to guide users through product selection.
### ⚡ Key Navigation Principles
1. **Question 1 (Start)**: Built from Column G (Base Type) - Door, Window, etc.
2. **Question 2 (Sub-Type)**: Built from Column H (Doors) OR Column I (Windows) based on Q1 selection
3. **Question 3 (Material)**: **CONDITIONAL** - Only show if sub-type has products with multiple materials (Aluminum AND Vinyl)
- Example: Storm Doors (all Aluminum) → SKIP this question
- Example: Mixed Windows (some Aluminum, some Vinyl) → SHOW this question
4. **Question 4 (Color)**: **CONDITIONAL** - Only show if filtered products have multiple color options
5. **Question 5 (Dimensions)**: Always show - final step before product display
6. **Dynamic Linking**: The "next" property must skip questions that aren't needed for that path
### Structure
```json
{
"start": {
"type": "question",
"inputType": "button",
"title": "What are you looking for?",
"subtitle": "Select the product category",
"answers": [
{
"caption": "PRODUCT_TYPE",
"image": "EMOJI",
"next": "NEXT_QUESTION_ID",
"filter": {
"baseType": "VALUE"
}
}
]
},
"q-QUESTION_ID": {
"type": "question",
"inputType": "button|form",
"title": "Question Title",
"subtitle": "Question subtitle",
"answers": [...],
"fields": [...]
}
}
```
### Generation Rules
#### Question 1: Base Type Selection (Start)
**Source**: Column G (Base Type)
1. Extract all unique values from column G where DISCONT=FALSE
2. Create button option for each unique base type (e.g., "Window", "Door")
3. Assign appropriate emojis (🪟 for windows, 🚪 for doors)
4. Each answer links to the corresponding sub-type question
**Example**: "What are you looking for?" → Window / Door options
#### Question 2: Sub-Type Selection
**Source**: Column H (for Doors) OR Column I (for Windows)
1. **For Door selection**: Use column H (Sub-type Door)
- Extract unique values where column G = "Door" and DISCONT=FALSE
- Examples: "Storm Door", "Patio Door", etc.
2. **For Window selection**: Use column I (Sub-type Window)
- Extract unique values where column G = "Window" and DISCONT=FALSE
- Examples: "Storm Window", "Casement", etc.
3. Create separate question branches:
- `q-door-type`: For door sub-types
- `q-window-type`: For window sub-types
4. Each answer links to either material question (if needed) or dimensions question
**Example**: "What type of door?" → Storm Door / Patio Door options
#### Question 3: Material Selection (CONDITIONAL)
**Source**: Columns L (Aluminum) and M (Vinyl)
**Important**: This question should **only appear** if products in the selected sub-type have multiple material options.
**Logic**:
1. For each sub-type group, count materials:
- Count products where Aluminum (column L) = TRUE
- Count products where Vinyl (column M) = TRUE
2. **Show material question** if:
- Some products have Aluminum=TRUE AND some have Vinyl=TRUE
- OR any single product has BOTH Aluminum=TRUE AND Vinyl=TRUE
3. **Skip material question** if:
- ALL products in the sub-type have only one material type
- Example: All "Storm Door" products only have Aluminum=TRUE → Skip material question
4. If shown, create radio buttons or select dropdown with:
- Only materials that exist in the sub-type group
- Link to color question or dimensions
**Example Skip Case**: Storm Doors (all Aluminum only) → Skip directly to dimensions
**Example Show Case**: Windows (some Aluminum, some Vinyl) → Show material selection
#### Question 4: Color Selection (CONDITIONAL)
**Source**: Color columns (N through S+)
Similar conditional logic as materials:
1. Only show if products in the filtered group have multiple color options
2. Present only colors available for the selected material (if material was selected)
3. Skip if all products have the same single color
#### Question 5: Dimensions Entry (Always Show)
**Source**: User input
1. Width input field (number, required)
2. Height input field (number, required)
3. This is typically the final question before showing filtered products
#### Filter Integration
Each answer should accumulate filter criteria:
```json
"filter": {
"baseType": "Window|Door",
"subType": "Storm Door|etc",
"material": "Aluminum|Vinyl", // Only if material question was shown
"color": "White|Bronze|etc", // Only if color question was shown
"category": "CATEGORY_CODE"
}
```
#### Navigation Flow Schema
```
start (Column G)
→ q-door-type (Column H) OR q-window-type (Column I)
→ q-material (Columns L,M) [CONDITIONAL - only if multiple materials exist]
→ q-color (Columns N+) [CONDITIONAL - only if multiple colors exist]
→ q-dimensions (User Input)
→ results (Filtered products)
```
**Dynamic Linking**: The "next" value for each question must be calculated based on whether the next conditional question is needed:
- If material question not needed → link sub-type directly to color or dimensions
- If color question not needed → link material (or sub-type) directly to dimensions
---
## Output File 2: `data/products.json`
### Purpose
Store all product data for display on product detail pages.
### Structure
```json
[
{
"id": "PROD_CODE",
"productCode": "PROD_CODE",
"category": "CATEGORY",
"description": "DESCRIPTION",
"discontinued": false,
"location": "LOC_CODE",
"baseType": "Window|Door",
"subType": {
"door": "VALUE_OR_NULL",
"window": "VALUE_OR_NULL"
},
"materials": ["Aluminum", "Vinyl"],
"colors": ["Black", "White", "Bronze", "Tan", "Mill", "Sandstone"],
"isAccessory": false,
"compatibleAccessories": ["PROD_CODE1", "PROD_CODE2"]
}
]
```
### Generation Rules
1. **Include all rows** where `DISCONT` (column A) is FALSE
2. **Skip accessories** (column J = TRUE) - those go in accessories.json
3. **Materials array**: Include all material columns (L+) where value is TRUE
4. **Colors array**: Include all color columns where value is TRUE
5. **baseType**: Copy from column G
6. **subType**: Create object with "door" and "window" keys from columns H and I
7. **compatibleAccessories**: Populate based on accessories linked to this product (see Accessories section)
---
## Output File 3: `data/accessories.json`
### Purpose
Store accessory/option data that can be applied to products (e.g., handles for storm doors, glass inserts).
### Structure
```json
[
{
"id": "PROD_CODE",
"accessoryCode": "PROD_CODE",
"category": "CATEGORY",
"description": "DESCRIPTION",
"discontinued": false,
"location": "LOC_CODE",
"baseType": "Window|Door",
"materials": ["Aluminum"],
"colors": ["Black", "White"],
"compatibilityRules": {
"type": "subType|category|specific",
"subTypeDoor": "Storm Door",
"subTypeWindow": null,
"categories": ["STD", "INSERTS"],
"specificProducts": ["6100I", "8100I"],
"requiresMatch": {
"material": true,
"color": false
}
},
"optionType": "handle|insert|hardware|glass",
"metadata": {
"specificItemLink": false
}
}
]
```
### Generation Rules
1. **Include all rows** where `Accessory Yes` (column J) is TRUE
2. **Skip discontinued** items (column A = TRUE)
3. **compatibilityRules.type**: Determine based on available data
- If column H (Sub-type Door) or column I (Sub-type Window) has value → "subType"
- If column K (This Item) is TRUE → "specific" (requires product linking)
- Otherwise → "category"
4. **compatibilityRules.subTypeDoor/Window**: Copy from columns H and I
5. **compatibilityRules.categories**: Extract from column D (CATEGORY)
6. **compatibilityRules.specificProducts**: To be populated based on column K logic
- If column K is TRUE, this accessory is for specific products
- You may need to analyze patterns in PROD_CODE or CATEGORY to determine relationships
- Example: "BGIST" (inserts for storm doors) might be compatible with all storm door products
7. **optionType**: Infer from DESCRIPTION or CATEGORY
- If DESCRIPTION contains "HANDLE" → "handle"
- If DESCRIPTION contains "INSERT" → "insert"
- If DESCRIPTION contains "HARDWARE" → "hardware"
- If DESCRIPTION contains "GLASS" → "glass"
- Default → "option"
8. **requiresMatch**: Set based on whether accessory materials/colors must match product
- For handles/hardware: material matching often required
- For inserts: usually flexible
---
## Multi-Group Assignment (Bit Values Consideration)
### Current Structure
Currently, each product belongs to a single category/sub-type based on columns G, H, and I.
### Proposed Enhancement: Bit Flags
To allow products to appear in multiple navigation groups, consider implementing bit flags for categories:
```json
{
"productCode": "EXAMPLE",
"description": "Multi-purpose product",
"categoryFlags": 7, // Binary: 111 (belongs to groups 1, 2, and 4)
"categoryBits": {
"residential": 1, // 2^0 = 1
"commercial": 2, // 2^1 = 2
"industrial": 4, // 2^2 = 4
"custom": 8 // 2^3 = 8
},
"navigationGroups": ["residential", "commercial", "industrial"]
}
```
### Implementation Options
#### Option A: Additional CSV Columns
Add bit value columns to track multiple group memberships:
- Column U: Navigation Group Bits (integer)
- Column V: Secondary Category
- Column W: Tertiary Category
#### Option B: Parse from Description/Category
Analyze DESCRIPTION and CATEGORY fields to identify products that could belong to multiple groups:
- Keywords indicating dual-purpose (e.g., "residential/commercial")
- Multiple category codes separated by delimiter
#### Option C: JSON-Only Enhancement
Generate single-group assignments from CSV, then manually or programmatically enhance JSON with additional group memberships based on business rules.
### Navigation Impact
With bit values:
1. Multiple sub-type paths could lead to the same product
2. Products appear in search results for multiple filter combinations
3. Requires modification to filter logic to use bitwise operations
**Example**: A storm door that works for both residential and commercial applications could appear in both navigation paths.
### Refinement Needed
This feature requires:
- ✅ Business rules for multi-group assignment
- ✅ Decision on implementation approach (CSV vs JSON)
- ✅ Filter logic updates to handle bitwise comparisons
- ✅ Testing to ensure products appear in correct groups
- ✅ UI considerations (showing product appears in multiple categories)
---
## Data Relationships
### Products ↔ Accessories Linking
1. **By Sub-Type**: Accessories with matching sub-type values (columns H or I) are compatible
- Example: Accessory with subTypeDoor="Storm Door" → compatible with all products where subType.door="Storm Door"
2. **By Category**: Accessories linked to specific CATEGORY codes
- Example: Accessory with category="INSERTS" might be compatible with products in "STD" category
3. **By Specific Product Code**: Use column K (This Item) flag
- If TRUE, requires manual mapping or pattern analysis
- Consider adding a "specificProductCodes" field to link directly
### Recommendation Algorithm
When displaying accessories for a product:
```
1. Match by specificProducts first (exact match)
2. Match by subType (door or window)
3. Match by category
4. Filter by material/color compatibility if requiresMatch is true
5. Exclude discontinued accessories
```
---
## Processing Steps
### Step 1: Parse CSV
1. Read products.csv starting from row 3 (skip header rows 1-2)
2. Split each row by comma delimiter
3. Handle empty fields appropriately
4. Parse boolean values (TRUE/FALSE → true/false in JSON)
### Step 2: Categorize Rows
1. Separate products (column J = FALSE) from accessories (column J = TRUE)
2. Filter out discontinued items (column A = TRUE) or include with flag
### Step 3: Extract Navigation Data
1. Collect unique values from columns G, H, I
2. For each sub-type group, analyze material and color diversity
3. Build question hierarchy with conditional logic:
- Level 1: Base Type (Window, Door) - from column G
- Level 2: Sub-Type (Storm Door, Casement, etc.) - from columns H/I
- Level 3: Material (CONDITIONAL) - from columns L, M
- Level 4: Color (CONDITIONAL) - from color columns
- Level 5: Dimensions & final inputs
4. Generate question flow with proper linking (next values)
#### Algorithm: Determine Material Question Necessity
```
For each sub-type group:
products = filter products where subType matches AND DISCONT=FALSE
aluminumCount = count products where Aluminum (col L) = TRUE
vinylCount = count products where Vinyl (col M) = TRUE
bothCount = count products where Aluminum=TRUE AND Vinyl=TRUE
IF (aluminumCount > 0 AND vinylCount > 0) OR bothCount > 0:
SHOW material question for this sub-type
Create q-material-{subtype} with options for available materials
ELSE:
SKIP material question
Link sub-type answer directly to color question or dimensions
Auto-apply the single material to filter
```
#### Algorithm: Determine Color Question Necessity
```
For each (sub-type, material) combination:
products = filter products where match AND DISCONT=FALSE
availableColors = []
For each color column (N through S+):
IF any product has this color = TRUE:
add color to availableColors
IF len(availableColors) > 1:
SHOW color question for this path
ELSE IF len(availableColors) = 1:
SKIP color question
Auto-apply the single color to filter
ELSE:
SKIP color question (no color data)
```
### Step 4: Build Products Array
1. For each non-accessory, non-discontinued row:
- Extract all product fields
- Parse material/color columns into arrays
- Create product object
- Add to products array
### Step 5: Build Accessories Array
1. For each accessory row:
- Extract accessory fields
- Determine compatibility rules
- Infer option type from description
- Create accessory object
- Add to accessories array
### Step 6: Link Products to Accessories
1. For each product, find compatible accessories based on:
- Sub-type matching
- Category matching
- Specific product code matching
2. Populate compatibleAccessories array in products.json
3. Verify bidirectional relationships
### Step 7: Validate Output
1. Ensure all JSON is valid and properly formatted
2. Check that all question flows have valid "next" links
3. Verify product-accessory relationships are logical
4. Confirm no duplicate IDs exist
---
## Advanced Considerations
### Column Extensions
If additional columns are added beyond column T (~):
- Check for additional material/color flags
- Look for price, availability, or specification data
- Include in metadata or as new product properties
### Future Enhancements
You may want to extend the schema with:
1. **Pricing**: Add price fields to products and accessories
2. **Images**: Add image URLs or filenames
3. **Specifications**: Add detailed specs (dimensions, ratings, etc.)
4. **Availability**: Add stock levels or lead times
5. **Sorting**: Add sort order or priority fields
---
## Example Outputs
### Example Product Object
```json
{
"id": "6100I",
"productCode": "6100I",
"category": "STD",
"description": "STAR 6100 FULL VIEW STORM DOOR",
"discontinued": false,
"location": "Iola",
"baseType": "Door",
"subType": {
"door": "Storm Door",
"window": null
},
"materials": ["Aluminum"],
"colors": ["White", "Bronze"],
"isAccessory": false,
"compatibleAccessories": ["BGIST", "TGIODD"]
}
```
### Example Accessory Object
```json
{
"id": "BGIST",
"accessoryCode": "BGIST",
"category": "INSERTS",
"description": "INSERTS FOR STORM DOORS",
"discontinued": false,
"location": "Iola",
"baseType": "Door",
"materials": ["Aluminum"],
"colors": ["Black", "White", "Bronze", "Mill", "Sandstone"],
"compatibilityRules": {
"type": "subType",
"subTypeDoor": "Storm Door",
"subTypeWindow": null,
"categories": ["STD", "PD"],
"specificProducts": [],
"requiresMatch": {
"material": true,
"color": false
}
},
"optionType": "insert",
"metadata": {
"specificItemLink": false
}
}
```
### Example Navigation Flow
```json
{
"start": {
"type": "question",
"inputType": "button",
"title": "What are you looking for?",
"subtitle": "Select the product category",
"answers": [
{
"caption": "Door",
"image": "🚪",
"next": "q-door-type",
"filter": { "baseType": "Door" }
},
{
"caption": "Window",
"image": "🪟",
"next": "q-window-type",
"filter": { "baseType": "Window" }
}
]
},
"q-door-type": {
"type": "question",
"inputType": "button",
"title": "What type of door?",
"subtitle": "Select the door category",
"answers": [
{
"caption": "Storm Door",
"image": "🚪",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Storm Door",
"material": "Aluminum"
},
"note": "Material question skipped - all storm doors are aluminum only"
},
{
"caption": "Patio Door",
"image": "🚪",
"next": "q-material-patio",
"filter": {
"baseType": "Door",
"subType": "Patio Door"
},
"note": "Material question shown - patio doors have multiple material options"
}
]
},
"q-material-patio": {
"type": "question",
"inputType": "button",
"title": "Select Material",
"subtitle": "Choose your preferred material for Patio Door",
"answers": [
{
"caption": "Aluminum",
"image": "🔩",
"next": "q-color-patio-aluminum",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum"
}
},
{
"caption": "Vinyl",
"image": "🪟",
"next": "q-color-patio-vinyl",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Vinyl"
}
}
],
"conditional": {
"showIf": "multipleOptionsExist",
"check": "materials",
"fallbackNext": "q-dimensions"
}
},
"q-color-patio-aluminum": {
"type": "question",
"inputType": "button",
"title": "Select Color",
"subtitle": "Choose your preferred color",
"answers": [
{
"caption": "White",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "White"
}
},
{
"caption": "Bronze",
"next": "q-dimensions",
"filter": {
"baseType": "Door",
"subType": "Patio Door",
"material": "Aluminum",
"color": "Bronze"
}
}
]
},
"q-dimensions": {
"type": "question",
"inputType": "form",
"title": "Enter Product Dimensions",
"subtitle": "Please provide the measurements",
"fields": [
{
"name": "width",
"label": "Width (inches)",
"type": "number",
"required": true,
"placeholder": "e.g., 36"
},
{
"name": "height",
"label": "Height (inches)",
"type": "number",
"required": true,
"placeholder": "e.g., 80"
}
],
"next": "results"
}
}
```
**Key Points in Example**:
- Storm Door skips material question (goes directly to dimensions)
- Patio Door shows material question (multiple materials available)
- Material selection leads to color-specific questions
- All paths eventually reach dimensions entry
---
## Final Notes
1. **Data Quality**: Some rows may have inconsistent data. Handle gracefully with defaults.
2. **Empty Values**: Treat empty strings as null in JSON
3. **Boolean Conversion**: CSV TRUE/FALSE should become JSON true/false
4. **Unique IDs**: PROD_CODE serves as the unique identifier
5. **Relationships**: The linking between products and accessories may require iterative refinement based on business rules
## Questions to Consider
When implementing, clarify:
1. Should discontinued items be included in ANY output file?
2. How should accessories with column K=TRUE be specifically linked?
3. Are there additional columns beyond column S that need parsing?
4. Should colors and materials be validated against a master list?
5. What should happen if a product has no compatible accessories?
6. **Should the navigation include filters for materials/colors early, or determine dynamically based on product availability?** ✅ RESOLVED: Dynamic based on availability
7. **For sub-types with only one material option, should that material be auto-applied to the filter?** → YES, skip question and auto-apply
8. **Multi-group assignment (bit values):**
- Should products be able to appear in multiple navigation paths?
- If yes, how should this be indicated in the CSV? (new column, description parsing, manual JSON editing?)
- What business rules determine multi-group membership?
- Should search results indicate a product appears in multiple categories?
9. **Material/Color question threshold:**
- Current logic: Show if > 1 option exists
- Alternative: Show only if > X% of products have multiple options (e.g., 20% threshold)
- Should we show questions even if only 1-2 products have alternative options?
---
## Success Criteria
The parsing is complete when:
- ✅ All three JSON files are generated and valid
- ✅ Products.json contains all non-accessory, non-discontinued products
- ✅ Accessories.json contains all accessory items with proper compatibility rules
- ✅ Navigation.json provides a complete question flow from start to product selection
- ✅ Product-accessory relationships are established and logical
- ✅ All materials and colors are properly extracted into arrays
- ✅ No data loss from the original CSV
+221
View File
@@ -0,0 +1,221 @@
# Bitwise Helper Files - Usage Guide
## 📁 Generated Files
1. **`data/product_bitwise.csv`** - CSV format with bit values for each product
2. **`data/product_bitwise.json`** - JSON format with bit values for each product
3. **`data/bitwise_legend.json`** - Complete bit mapping documentation
## 🔢 Bit System Overview
Each product gets a single integer value that encodes multiple attributes using bitwise flags.
### Bit Positions (0-11): Base Attributes, Materials, Colors
| Bit | Value | Attribute |
|-----|-------|-----------|
| 0 | 1 | Door |
| 1 | 2 | Window |
| 2 | 4 | Is Accessory |
| 3 | 8 | Specific Item Link |
| 4 | 16 | Aluminum Material |
| 5 | 32 | Vinyl Material |
| 6 | 64 | Black Color |
| 7 | 128 | White Color |
| 8 | 256 | Bronze Color |
| 9 | 512 | Tan Color |
| 10 | 1024 | Mill Color |
| 11 | 2048 | Sandstone Color |
### Bit Positions (12+): Sub-types
| Bit | Value | Sub-type |
|-----|-------|----------|
| 12 | 4096 | Patio Door |
| 13 | 8192 | Primary Window |
| 14 | 16384 | Storm Door |
| 15 | 32768 | Storm Window |
## 💡 Usage Examples
### Example 1: Storm Door Insert (BGIST)
```
Product Code: BGIST
Description: INSERTS FOR STORM DOORS
Bit Value: 17553
Binary: 0100010010010001
Decoded:
✓ Door (bit 0 = 1)
✗ Window (bit 1 = 0)
✗ Accessory (bit 2 = 0)
✗ Specific Item (bit 3 = 0)
✓ Aluminum (bit 4 = 1)
✗ Vinyl (bit 5 = 0)
✓ Black (bit 6 = 1)
✓ White (bit 7 = 1)
✓ Bronze (bit 8 = 1)
✗ Tan (bit 9 = 0)
✓ Mill (bit 10 = 1)
✓ Sandstone (bit 11 = 1)
✗ Patio Door (bit 12 = 0)
✗ Primary Window (bit 13 = 0)
✓ Storm Door (bit 14 = 1)
✗ Storm Window (bit 15 = 0)
```
### Example 2: Check Attributes in Code
#### Python
```python
import json
# Load helper file
with open('data/product_bitwise.json') as f:
products = json.load(f)
# Get a product
product = next(p for p in products if p['PROD_CODE'] == 'BGIST')
bit_value = product['BIT_VALUE']
# Check individual flags
is_door = bool(bit_value & 1)
is_aluminum = bool(bit_value & 16)
is_white = bool(bit_value & 128)
is_storm_door = bool(bit_value & 16384)
print(f"BGIST is door: {is_door}")
print(f"BGIST is aluminum: {is_aluminum}")
print(f"BGIST is white: {is_white}")
print(f"BGIST is storm door subtype: {is_storm_door}")
# Filter products by multiple criteria
# Example: Find all aluminum doors with white color
aluminum_white_doors = [
p for p in products
if (p['BIT_VALUE'] & 1) and # Is a door
(p['BIT_VALUE'] & 16) and # Has aluminum
(p['BIT_VALUE'] & 128) and # Has white
not p['DISCONTINUED'] # Not discontinued
]
print(f"Found {len(aluminum_white_doors)} aluminum white doors")
```
#### JavaScript
```javascript
// Load the JSON file
fetch('data/product_bitwise.json')
.then(response => response.json())
.then(products => {
// Check individual flags
const product = products.find(p => p.PROD_CODE === 'BGIST');
const bitValue = product.BIT_VALUE;
const isDoor = !!(bitValue & 1);
const isAluminum = !!(bitValue & 16);
const isWhite = !!(bitValue & 128);
const isStormDoor = !!(bitValue & 16384);
console.log(`BGIST is door: ${isDoor}`);
console.log(`BGIST is aluminum: ${isAluminum}`);
console.log(`BGIST is white: ${isWhite}`);
console.log(`BGIST is storm door: ${isStormDoor}`);
// Filter products
const aluminumWhiteDoors = products.filter(p =>
(p.BIT_VALUE & 1) && // Is a door
(p.BIT_VALUE & 16) && // Has aluminum
(p.BIT_VALUE & 128) && // Has white
!p.DISCONTINUED // Not discontinued
);
console.log(`Found ${aluminumWhiteDoors.length} aluminum white doors`);
});
```
## 🎯 Benefits of Bitwise Classification
### 1. **Multi-Group Assignment**
Products can belong to multiple categories simultaneously:
- A product can be both Door AND Window (rare but possible)
- A product can have multiple materials (Aluminum AND Vinyl)
- A product can have multiple colors
### 2. **Fast Filtering**
Bitwise operations are extremely fast:
```python
# Instead of:
if product.baseType == 'Door' and 'Aluminum' in product.materials and 'White' in product.colors:
# Use:
if (bit_value & 1) and (bit_value & 16) and (bit_value & 128):
```
### 3. **Compact Storage**
One integer stores multiple attributes:
- Single integer vs. multiple boolean fields
- Easy to index and search in databases
- Efficient for large datasets
### 4. **Easy Matching**
Perfect for accessory compatibility:
```python
# Check if accessory matches product materials
accessory_materials = 48 # Aluminum (16) + Vinyl (32)
product_materials = 16 # Aluminum only
# Check if they share any materials
if accessory_materials & product_materials:
print("Compatible!") # True because both have Aluminum
```
## 🔄 Integration with Navigation System
Use bitwise values to:
1. Dynamically generate navigation options
2. Filter products in real-time based on user selections
3. Match accessories to products efficiently
4. Handle complex "OR" queries (multiple categories)
### Example: Dynamic Material Question
```python
# Get all products for "Storm Door" subtype
storm_door_products = [p for p in products if p['BIT_VALUE'] & 16384]
# Check materials
has_aluminum = any(p['BIT_VALUE'] & 16 for p in storm_door_products)
has_vinyl = any(p['BIT_VALUE'] & 32 for p in storm_door_products)
# Show material question only if multiple materials exist
if has_aluminum and has_vinyl:
show_material_question()
else:
skip_to_next_question()
```
## 📝 Maintenance
### Regenerating Helper Files
When products.csv changes:
```bash
python generate_bitwise_helper.py
```
### Adding New Attributes
To add new bit flags, edit `generate_bitwise_helper.py`:
1. Add to `BIT_DEFINITIONS` dictionary
2. Update `calculate_bit_value()` function
3. Regenerate files
### Adding New Sub-types
Sub-types are automatically detected! Just add them to columns H or I in the CSV, then regenerate.
## 🚀 Performance Notes
- Bitwise AND (`&`) - Check if flag is set: `bit_value & 16`
- Bitwise OR (`|`) - Combine flags: `16 | 128` = Aluminum + White
- Bitwise NOT (`~`) - Invert flags (advanced)
- Bitwise XOR (`^`) - Toggle flags (advanced)
All bitwise operations are O(1) - constant time!
+238
View File
@@ -0,0 +1,238 @@
# Deployment Guide - Files to Upload to Server
## 🚀 Updated Files for Login System & Permission System
To deploy the new login, location selection, user management, and permission system to your server, you'll need to upload the following files:
### ✅ Essential Updated Files
#### **1. Main Application Files**
These core files have been modified and MUST be uploaded:
- `app/app.py` - Main Flask application with authentication, sessions, and permissions
- `app/config.py` - Configuration (verify SECRET_KEY is set)
- `app/requirements.txt` - Python dependencies (may need to run `pip install -r requirements.txt` on server)
#### **2. HTML Templates** (all in `app/templates/`)
- `app/templates/login.html` - Login page
- `app/templates/select_location.html` - Location selection page (with User Management link)
- `app/templates/user_manager.html` - User management interface (with password change)
- `app/templates/index2.html` - Main app with user info header
- `app/templates/access_denied.html` - Permission denied page
#### **3. CSS Files**
- `app/css/styles.css` - Updated styles for user-info-bar and logout button
#### **4. User Data**
- `app/data/users.json` - User accounts with permissions
- `app/data/example_user_structure.json` - Example data format (documentation only)
⚠️ **Important**: If you have existing users on the server, back them up first, then merge the permission structure into existing user accounts.
### 📁 New Folders/Files Created
#### **Admin Utilities** (optional, but recommended)
- `app/admin/` - New folder
- `app/admin/README.md` - Admin utilities documentation
- `app/admin/fix_default_locations.py` - User maintenance tool
- `app/admin/test_password_security.py` - Password security demo
⚠️ **Security Note**: The `app/admin/` folder should NOT be web-accessible. Configure your server to block access to this directory.
### 📚 Documentation Files (moved to information/)
These files are for reference only and do NOT need to be uploaded to the production server:
- `information/LOGIN_SYSTEM_README.md`
- `information/PERMISSIONS_SYSTEM.md`
- `information/PERMISSIONS_QUICKSTART.md`
- `information/USER_MANAGEMENT_README.md`
### 🔧 Server Configuration
#### **Python Dependencies**
After uploading files, install/update dependencies on the server:
```bash
cd /path/to/app
pip install -r requirements.txt
```
**Key dependencies** (will be installed from requirements.txt):
- Flask >= 3.0.0
- Werkzeug (for password hashing)
#### **Secret Key Configuration**
Ensure `app/config.py` has a strong SECRET_KEY:
```python
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-production-secret-key-here'
```
For production, use environment variable or generate with:
```python
import secrets
print(secrets.token_urlsafe(32))
```
#### **File Permissions**
Set appropriate permissions on the server:
```bash
# Data directory should be writable by the web server
chmod 755 app/data/
chmod 644 app/data/users.json
# Admin directory should NOT be web accessible
chmod 700 app/admin/
```
#### **WSGI Configuration**
If using WSGI (Passenger, uWSGI, etc.):
- `app/passenger_wsgi.py` - Already exists (Passenger)
- `app/wsgi.py` - Already exists (generic WSGI)
Make sure your server is configured to use the appropriate WSGI file.
### 🔐 Security Checklist Before Deployment
- [ ] Change SECRET_KEY in config.py to a secure random value
- [ ] Verify users.json has ONLY hashed passwords (no plain text)
- [ ] Block web access to `/app/admin/` directory
- [ ] Block web access to `/app/data/` directory (except through API)
- [ ] Enable HTTPS/SSL on the server
- [ ] Set `SESSION_COOKIE_SECURE = True` in config.py if using HTTPS
- [ ] Set appropriate file permissions (755/644)
- [ ] Test login functionality after deployment
- [ ] Verify permissions system works (try accessing /users without permission)
### 📤 Upload Methods
#### **Option 1: FTP/SFTP**
Upload all the files listed above using your FTP client, maintaining the directory structure.
#### **Option 2: Git**
If using Git:
```bash
git add app/app.py app/templates/* app/css/* app/data/users.json app/admin/*
git commit -m "Add login system, permissions, and user management"
git push
```
Then on server:
```bash
git pull
pip install -r app/requirements.txt
# Restart web server
```
#### **Option 3: ZIP Archive**
Create a ZIP of the entire `app/` folder and extract on the server.
### 🔄 Migration Steps for Existing Server
If you already have a running server:
1. **Backup Current Installation**
```bash
cp -r app/ app_backup_$(date +%Y%m%d)/
```
2. **Upload New Files**
Upload all files listed in "Essential Updated Files" section
3. **Update Dependencies**
```bash
pip install -r app/requirements.txt
```
4. **Update Existing Users** (if applicable)
If you have existing users without the permission structure, add permissions:
```json
{
"username": "existing_user",
"password": "existing_hash",
"permissions": {
"manage_users": false,
"view_reports": true,
"create_quotes": true
}
}
```
5. **Test in Maintenance Mode**
- Test login at `/login`
- Test user management at `/users` (as admin)
- Test main app still works
- Test permission checks
6. **Restart Web Server**
```bash
# Apache with Passenger
touch tmp/restart.txt
# Or systemctl
sudo systemctl restart your-service-name
```
### 🧪 Post-Deployment Testing
Test these flows after deployment:
1. **Login Flow**
- Navigate to `/login`
- Login with correct credentials
- Verify redirect to location selection (if multiple locations)
- Verify redirect to main app
2. **Permission System**
- Login as admin user (Master)
- Access `/users` - should work
- Login as non-admin user
- Try to access `/users` - should see "Access Denied"
3. **Password Change**
- Login as admin
- Go to User Management
- Click "Change Password" on a user
- Verify your password is required
- Change password and verify new password works
4. **Location Selection**
- Login as user with multiple locations
- Verify location selection page shows
- Verify User Management button shows only for admins
- Select location and verify redirect to main app
### ❗ Troubleshooting
**"500 Internal Server Error" after deployment:**
- Check server error logs
- Verify SECRET_KEY is set
- Ensure all dependencies installed
- Check file permissions
**"Users not loading" or "No users shown":**
- Verify `data/users.json` uploaded correctly
- Check JSON format is valid
- Ensure web server can read the file
**"Permission denied" when accessing files:**
- Check file ownership (should be web server user)
- Set correct permissions (755 for directories, 644 for files)
**Session not persisting:**
- Verify SECRET_KEY is consistent
- Check cookie settings in config
- Ensure HTTPS if SESSION_COOKIE_SECURE is True
### 📞 Support
After deployment, keep these files handy:
- Server error logs (usually in `/var/log/apache2/` or similar)
- `information/LOGIN_SYSTEM_README.md` - Login system documentation
- `information/PERMISSIONS_SYSTEM.md` - Permission system documentation
### 🎉 Success Indicators
You'll know deployment was successful when:
- ✅ Accessing `/` redirects to `/login` (if not logged in)
- ✅ Login with correct credentials works
- ✅ Master user can access `/users`
- ✅ Non-admin users see "Access Denied" at `/users`
- ✅ User info header shows in main app
- ✅ Logout works and redirects to login
- ✅ Password change requires admin verification
+476
View File
@@ -0,0 +1,476 @@
# Dynamic Image Generation API
## Overview
This system generates product images on-the-fly with configurable colors, hardware positions, and other options. It uses server-side image processing with PIL/Pillow to composite and recolor product images dynamically.
## Setup
### 1. Install Dependencies
```bash
pip install -r requirements.txt
```
This will install:
- Flask (web framework)
- Pillow (image processing)
- Other dependencies
### 2. Prepare Product Images
For the COBRAI demo product:
1. Save the white storm door image as `app/images/cobrai.jpg`
2. The configuration is already in `app/data/image_configs.json`
### 3. Create Cache Directory
The cache directory will be created automatically when the app runs:
```
app/cache/product_images/
```
## API Endpoints
### 1. Generate Product Image
**Endpoint:** `GET /api/product-image/<product_code>`
**Parameters:**
- `color` - Color name (default: 'white')
- Options: white, black, bronze, sandstone
- `hinge` - Hinge side (default: 'right')
- Options: left, right
- `material` - Material type (default: 'aluminum')
- For future use
- `format` - Return format (default: 'image')
- Options: image, json
- `cache` - Use caching (default: 'true')
- Options: true, false
**Examples:**
Return image directly (for use in `<img>` tags):
```
GET http://localhost:8080/api/product-image/cobrai?color=black&hinge=right
GET http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left
```
Return JSON with base64 image:
```
GET http://localhost:8080/api/product-image/cobrai?color=white&hinge=right&format=json
```
**Response (format=image):**
Raw PNG image file
**Response (format=json):**
```json
{
"status": "success",
"productCode": "cobrai",
"color": "black",
"hinge": "left",
"material": "aluminum",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"format": "base64"
}
```
### 2. Get Product Configuration
**Endpoint:** `GET /api/product-config/<product_code>`
Returns the complete configuration for a product including available colors, colorable regions, hardware positions, etc.
**Example:**
```
GET http://localhost:8080/api/product-config/cobrai
```
**Response:**
```json
{
"status": "success",
"productCode": "cobrai",
"config": {
"sourceImage": "images/cobrai.jpg",
"productCode": "COBRAI",
"availableColors": {
"white": {"rgb": [255, 255, 255], "name": "White"},
"black": {"rgb": [30, 30, 30], "name": "Black"},
"bronze": {"rgb": [110, 80, 50], "name": "Bronze"}
},
"colorableRegions": [...],
"hardwarePositions": {...},
"metadata": {...}
}
}
```
### 3. Clear Image Cache
**Endpoint:** `POST /api/clear-image-cache`
Clears cached images. Useful when updating source images or configurations.
**Request Body (Optional):**
```json
{
"productCode": "cobrai"
}
```
**Response:**
```json
{
"status": "success",
"message": "Cache cleared for cobrai"
}
```
## Configuration File Format
Location: `app/data/image_configs.json`
### Structure:
```json
{
"product_code": {
"sourceImage": "path/to/source.jpg",
"productCode": "PRODUCT_CODE",
"description": "Product description",
"availableColors": {
"color_name": {
"rgb": [R, G, B],
"name": "Display Name"
}
},
"colorableRegions": [
{
"name": "region_identifier",
"topLeft": [x, y],
"bottomRight": [x, y],
"description": "What this region is"
}
],
"hardwarePositions": {
"handle_right": {
"x": 580,
"y": 730,
"image": "path/to/hardware.png",
"flip": false
}
},
"glassRegions": [
{
"name": "glass_area",
"topLeft": [x, y],
"bottomRight": [x, y],
"description": "Glass/screen area"
}
],
"metadata": {
"imageWidth": 720,
"imageHeight": 1450,
"defaultColor": "white",
"defaultHinge": "right"
},
"cache": {
"enabled": true,
"directory": "cache/product_images",
"maxAge": 86400
}
}
}
```
### Key Concepts:
#### Colorable Regions
Define rectangular areas to be recolored. The algorithm:
1. Preserves brightness/luminosity
2. Applies target color
3. Maintains shadows and highlights
Coordinates are `[x, y]` where:
- `topLeft`: Upper-left corner
- `bottomRight`: Lower-right corner
#### Hardware Positions
Define where handles, locks, etc. should be placed:
- `x, y`: Position to place hardware image
- `image`: Path to hardware PNG (with transparency)
- `flip`: Whether to flip horizontally (for left hinge)
#### Glass Regions
Define areas that should remain unchanged (glass, screens).
Currently for documentation purposes; future feature.
## Frontend Integration
### Option 1: Direct Image URL
```html
<img src="/api/product-image/cobrai?color=black&hinge=left" alt="Storm Door">
```
### Option 2: JavaScript with Base64
```javascript
async function loadProductImage(productCode, color, hinge) {
const response = await fetch(
`/api/product-image/${productCode}?color=${color}&hinge=${hinge}&format=json`
);
const data = await response.json();
if (data.status === 'success') {
document.getElementById('product-img').src = data.image;
}
}
// Usage
loadProductImage('cobrai', 'black', 'left');
```
### Option 3: Dynamic URL Switching
```javascript
function updateProductImage(color, hinge) {
const img = document.getElementById('product-img');
img.src = `/api/product-image/cobrai?color=${color}&hinge=${hinge}`;
}
// On color change
document.getElementById('color-select').addEventListener('change', (e) => {
const color = e.target.value;
const hinge = document.querySelector('[name="hinge"]:checked').value;
updateProductImage(color, hinge);
});
```
## How Image Processing Works
### Color Application
1. **Load base image** - White or neutral colored product photo
2. **Define regions** - Specify rectangles for frame, panels, etc.
3. **Calculate brightness** - For each pixel, determine relative brightness
4. **Apply target color** - Colorize while preserving brightness variations
5. **Result** - Natural-looking colored product with preserved shadows/highlights
### Brightness Preservation
```python
# For each pixel in region:
original_brightness = (r + g + b) / 3
brightness_factor = original_brightness / 255.0
new_r = target_color_r * brightness_factor
new_g = target_color_g * brightness_factor
new_b = target_color_b * brightness_factor
```
This maintains shadows (darker pixels stay darker) and highlights (lighter pixels stay lighter).
### Hardware Application
1. Load hardware PNG with transparency
2. Optionally flip horizontally for left hinge
3. Composite onto product image at specified position
### Caching
- Generated images are cached using MD5 hash of parameters
- Cache key: `{product_code}_{color}_{hinge}_{material}`
- Stored as PNG in `cache/product_images/`
- Subsequent requests return cached version instantly
## Testing the Endpoints
### Using cURL
Test basic image generation:
```bash
curl http://localhost:8080/api/product-image/cobrai?color=black > test_black.png
```
Test with different configurations:
```bash
curl http://localhost:8080/api/product-image/cobrai?color=bronze&hinge=left > test_bronze_left.png
```
Get JSON response:
```bash
curl http://localhost:8080/api/product-image/cobrai?color=white&format=json
```
Get configuration:
```bash
curl http://localhost:8080/api/product-config/cobrai
```
Clear cache:
```bash
curl -X POST http://localhost:8080/api/clear-image-cache -H "Content-Type: application/json" -d '{"productCode":"cobrai"}'
```
### Using Browser
Simply visit:
```
http://localhost:8080/api/product-image/cobrai?color=black&hinge=left
```
### Using JavaScript Fetch
```javascript
// Get image as blob
fetch('/api/product-image/cobrai?color=bronze&hinge=left')
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.getElementById('img').src = url;
});
// Get as JSON
fetch('/api/product-image/cobrai?color=black&format=json')
.then(response => response.json())
.then(data => {
document.getElementById('img').src = data.image;
});
```
## Adding New Products
### Step 1: Prepare Source Image
- Photograph product in white or neutral color
- Clean background
- Good lighting
- High resolution (will be resized)
- Save as JPG in `app/images/`
### Step 2: Determine Regions
Open image in image editor and note pixel coordinates:
- Frame edges (left, right, top, bottom)
- Panels/kickplates
- Rails/dividers
Record as `topLeft [x, y]` and `bottomRight [x, y]`
### Step 3: Add to Configuration
Add entry to `image_configs.json`:
```json
{
"your_product": {
"sourceImage": "images/your_product.jpg",
"productCode": "YOUR-PRODUCT",
"availableColors": {
"white": {"rgb": [255, 255, 255], "name": "White"},
"black": {"rgb": [30, 30, 30], "name": "Black"}
},
"colorableRegions": [
{
"name": "frame",
"topLeft": [0, 0],
"bottomRight": [100, 1450]
}
],
"metadata": {
"imageWidth": 720,
"imageHeight": 1450,
"defaultColor": "white"
}
}
}
```
### Step 4: Test
```bash
curl http://localhost:8080/api/product-image/your_product?color=black > test.png
```
## Performance Notes
### First Request
- ~100-500ms (depending on image size and region count)
- Includes image loading, processing, and caching
### Cached Requests
- ~10-50ms
- Just file system read and serve
### Memory Usage
- Base image kept in memory during processing
- Minimal memory footprint when served from cache
### Optimization Tips
1. **Use caching** - Enabled by default
2. **Smaller source images** - 1000x1600px is usually sufficient
3. **Fewer regions** - Combine adjacent areas when possible
4. **CDN** - Serve cached images from CDN in production
## Production Deployment
### Recommended Setup
1. **Pre-generate** common combinations on deploy
2. **CDN** to serve cached images
3. **Redis cache** instead of filesystem (optional)
4. **Image optimization** - Use WebP format where supported
5. **Rate limiting** on generation endpoint
### Pre-generation Script
```python
from image_generator import ProductImageGenerator
generator = ProductImageGenerator()
products = ['cobrai', 'product2']
colors = ['white', 'black', 'bronze']
hinges = ['left', 'right']
for product in products:
for color in colors:
for hinge in hinges:
img = generator.generate_product_image(product, color, hinge)
print(f'Generated: {product} {color} {hinge}')
```
## Troubleshooting
### "PIL/Pillow not installed"
```bash
pip install Pillow
```
### "Product not found in configuration"
Check that product code in URL matches key in `image_configs.json`
### "Could not load base image"
Verify `sourceImage` path in config and that file exists
### Colors look wrong
Adjust RGB values in `availableColors` section
### Regions not coloring
1. Verify coordinates are within image bounds
2. Check that region isn't transparent
3. Ensure topLeft is actually top-left of bottomRight
### Cache not working
Check that `cache/product_images/` directory is writable
## Future Enhancements
### Planned Features
- [ ] Material textures (wood grain, brushed metal)
- [ ] Glass tinting/color
- [ ] Shadow/lighting adjustments based on color
- [ ] Multiple hardware styles
- [ ] Size variations
- [ ] Decorative glass patterns
- [ ] WebP format support
- [ ] Batch generation CLI tool
- [ ] Admin UI for region configuration
- [ ] Automatic region detection (AI/ML)
### Integration Ideas
- Direct integration with product configurator
- Real-time preview as user selects options
- Download high-res configured images
- Email configured image to customer
- Social media sharing with custom image
+81
View File
@@ -0,0 +1,81 @@
# Workspace Folder Structure
This workspace is organized into clean, logical folders:
## 📁 Root Directory
```
├── .env.example # Environment configuration template
├── .gitignore # Git ignore rules
├── .vscode/ # VS Code settings and tasks
├── app/ # 🚀 Active project files (MAIN APPLICATION)
└── information/ # 📚 Documentation and deprecated files
```
## 🚀 app/ - Active Project Files
**All working application code and assets**
- **app.py** - Main Flask application
- **config.py** - Application configuration
- **wsgi.py** / **passenger_wsgi.py** - Production WSGI servers
- **requirements.txt** - Python dependencies
- **parse_csv_to_json.py** - CSV to JSON data processor
- **generate_bitwise_helper.py** - Bitwise filtering data generator
- **css/** - Stylesheets
- **js/** - JavaScript files
- **templates/** - HTML templates
- **data/** - JSON data files
- **images/** - Image assets
## 📚 information/ - Documentation
**All project documentation and guides**
- AI_PARSING_INSTRUCTIONS.md
- BITWISE_USAGE_GUIDE.md
- QUICKSTART.md
- README.md
- REQUIRED_CODE_CHANGES.md
- START_HERE.md
- URL_EXAMPLES.md
- URL_IMPLEMENTATION_SUMMARY.md
- URL_STATE_MANAGEMENT.md
### information/deprecated/ - Old Files
**Deprecated files kept for reference**
- index.html / index2.html (replaced by templates/)
- codes.text (old data)
- run.bat (old batch file)
- Procfile (old deployment config)
- temp/ (temporary files)
---
## Working with the Project
### Running Tasks
All VS Code tasks are configured to work with the new structure:
- **Ctrl+Shift+B** - Process All Data (updates JSON from CSV)
- **Ctrl+Shift+P** → "Tasks: Run Task" → "Start Flask Server"
### Updating Data
When you update `app/data/products.csv`:
1. Press **Ctrl+Shift+B** to run "Process All Data"
2. This generates fresh JSON files in `app/data/`
### Starting the Server
1. Press **Ctrl+Shift+P**
2. Type "Tasks: Run Task"
3. Select "Start Flask Server"
4. Open http://127.0.0.1:8080
---
**Last Updated:** March 26, 2026
+195
View File
@@ -0,0 +1,195 @@
# Layered Image System Guide
## Overview
The product finder supports a layered image system that allows dynamic product configuration (changing colors, materials, hinge location, etc.) without requiring a separate photo for every combination.
## How It Works
### System Architecture
Images are stacked in layers (like Photoshop layers):
1. **Base Layer** - House/frame (JPG) - the static background
2. **Door Layer** - The door panel (PNG with transparency) - changes with color selection
3. **Hardware Layer** - Handle/lock set (PNG with transparency) - can flip for left/right hinge
4. **Overlay Layer** - Glass view/decorative elements (PNG with transparency) - optional
### Fallback Behavior
- **No layered config**: Shows the standard flat `image` field
- **Layered enabled but missing files**: Shows base layer + warning banner
- **User selects unavailable option**: Displays "Preview not available for this configuration"
## JSON Configuration
### Standard Product (Flat Image)
```json
{
"productCode": "450",
"description": "#450 RAVEN STORM WINDOWS",
"image": "images/450.jpg",
"colors": ["White", "Black", "Bronze"],
"materials": ["Aluminum"]
}
```
### Product with Layered Images
```json
{
"productCode": "404",
"description": "#404 FALCON STORM WINDOWS",
"image": "images/404.jpg",
"colors": ["White", "Black", "Bronze", "Sandstone"],
"materials": ["Aluminum"],
"imageConfig": {
"layered": true,
"basePath": "images/products/404/",
"layers": {
"base": "base.jpg",
"door": {
"white": "door-white.png",
"black": "door-black.png",
"bronze": "door-bronze.png",
"sandstone": "door-sandstone.png"
},
"hardware": "handle.png",
"overlay": {
"inside": "view-inside.png",
"outside": "view-outside.png"
}
}
}
}
```
### Partial Layered Images (Some Colors Available)
```json
{
"productCode": "505",
"description": "#505 DOOR",
"image": "images/505.jpg",
"colors": ["White", "Black", "Bronze", "Tan"],
"materials": ["Aluminum", "Vinyl"],
"imageConfig": {
"layered": true,
"basePath": "images/products/505/",
"layers": {
"base": "base.jpg",
"door": {
"white": "door-white.png",
"black": "door-black.png"
// Bronze and Tan not available yet - will show warning
},
"hardware": "handle.png"
}
}
}
```
## File Structure
### Recommended Directory Layout
```
app/images/products/
├── 404/
│ ├── base.jpg # Frame/house background
│ ├── door-white.png # White door panel (transparent BG)
│ ├── door-black.png # Black door panel (transparent BG)
│ ├── door-bronze.png # Bronze door panel (transparent BG)
│ ├── door-sandstone.png # Sandstone door panel (transparent BG)
│ ├── handle.png # Hardware (transparent BG)
│ ├── view-inside.png # Optional inside view overlay
│ └── view-outside.png # Optional outside view overlay
├── 450/
│ ├── base.jpg
│ └── door-white.png
└── [other products]/
```
## Creating Layered Images
### Requirements
- **Base image**: JPG format, includes frame, glass, background
- **Layer images**: PNG format with transparency
- **Consistent dimensions**: All layers for a product should be the same size
- **Alignment**: Layers must align perfectly when stacked
### Photoshop/GIMP Workflow
1. Start with full product photo
2. Create separate layers for each component
3. Remove background from door/hardware layers
4. Export:
- Base layer → JPG
- Component layers → PNG (with transparency)
5. Create color variants by adjusting door layer
### Photography Tips
- Use consistent lighting
- Photograph against neutral background (for easy removal)
- Keep camera/product position identical for all shots
- Consider photographing white version first, then recolor digitally
## Features
### Auto-Selection
- If product has only one color: auto-selected and dropdown disabled
- If no colors available: shows "N/A" and disabled
### Hinge Location
- Right/Left hinge radio buttons flip the hardware layer horizontally
- Works with both flat and layered images
### Dynamic Updates
- Color changes update the door layer instantly
- Missing images show warning instead of breaking
### Sorting
- Materials: Alphabetically sorted
- Colors: Alphabetically sorted with White always at bottom
## Testing Your Setup
### 1. Test Flat Fallback
Set `"layered": false` or remove `imageConfig` entirely - should show standard image
### 2. Test Missing Layer
Remove a color file - should show base + warning banner
### 3. Test All Colors
Select each color - should swap door layer smoothly
### 4. Test Hinge Flip
Toggle left/right hinge - hardware should flip horizontally
## Troubleshooting
### Images not showing
- Check file paths in `basePath` and layer filenames
- Verify files exist in `app/images/products/[code]/`
- Check browser console for 404 errors
### Colors not matching
- Ensure color keys in JSON match available color names
- Keys should be lowercase in the door config (e.g., `"white"` not `"White"`)
### Layers misaligned
- All images must be same dimensions
- Check that transparent PNGs aren't cropped differently
### Warning banner always showing
- Verify the selected color exists in `layers.door` object
- Check that color value from dropdown matches JSON key
## Migration Strategy
### Phase 1: Keep Flat Images
Keep existing flat images as fallback while creating layered versions
### Phase 2: Add Layered for Key Products
Focus on best-selling products first, add `imageConfig` gradually
### Phase 3: Full Migration
Once all images ready, can remove flat images (but recommend keeping as fallback)
## Performance Notes
- PNG layers are cached by browser
- Base image loads once, only door layer changes on color switch
- Much smaller file size than separate photos for each combination
- Example: Instead of 4 full photos (1MB each = 4MB), use 1 base (800KB) + 4 doors (200KB each = 800KB) = 1.6MB total
+312
View File
@@ -0,0 +1,312 @@
# Login System Documentation
## Overview
A complete authentication system for the CGW Product Finder that integrates with the user management system. Users must log in with their credentials to access the product finder application.
## Features
### 🔐 Secure Authentication
- Password verification using PBKDF2-SHA256 hashing
- Session-based authentication with HTTP-only cookies
- Automatic session management
- Active/inactive user account checking
### 📍 Multi-Location Support
- Automatic location selection for single-location users
- Location selection page for users with multiple accessible locations
- Default location preference
- Location-based access control
### 🛡️ Security Features
- Login required decorator protects all main routes
- Inactive accounts are automatically blocked
- Sessions expire on logout or server restart
- Secure session cookies (HTTP-only, SameSite)
## User Flow
### 1. Login Process
1. User visits the app → Redirected to `/login`
2. User enters **username** and **password**
3. System validates credentials against `users.json`
4. System checks if user account is **active**
5. If valid and active:
- Session is created
- User accessible locations are loaded
### 2. Location Selection (if applicable)
- **Single Accessible Location**: User goes directly to main app (location selection bypassed)
- **Multiple Accessible Locations**: User is redirected to `/select-location`
- Shows all accessible locations
- Default location is pre-selected
- User can choose their working location
- Selection is saved to session
- **Note**: Default location is automatically marked as accessible when user is created
### 3. Main Application Access
- User accesses the Product Finder
- User info displayed in header (username + current location)
- Logout button available in header
## Routes
### Public Routes (No Login Required)
- `GET /login` - Login page
- `POST /api/login` - Login endpoint
- `GET /users` - User management page
### Protected Routes (Login Required)
- `GET /` - Main product finder app
- `GET /quiz` - Quiz page (alias for main app)
- `GET /image-test` - Image generation test page
- `GET /select-location` - Location selection page
- `POST /api/select-location` - Set current location
### Session Routes
- `GET /api/session` - Get current session info
- `POST /api/logout` - Logout and clear session
## API Endpoints
### POST /api/login
Authenticate user and create session.
**Request:**
```json
{
"username": "john_doe",
"password": "password123"
}
```
**Success Response:**
```json
{
"status": "success",
"message": "Login successful",
"requiresLocationSelection": true
}
```
**Error Responses:**
```json
{
"status": "error",
"message": "Invalid username or password"
}
```
```json
{
"status": "error",
"message": "Account is inactive. Please contact an administrator."
}
```
### GET /api/session
Get current user session information.
**Response:**
```json
{
"status": "success",
"username": "john_doe",
"defaultLocation": "LINDS",
"currentLocation": "KC",
"accessibleLocations": ["LINDS", "KC", "IOLA"]
}
```
### POST /api/select-location
Select a location for the current session.
**Request:**
```json
{
"location": "KC"
}
```
**Response:**
```json
{
"status": "success",
"message": "Location selected",
"currentLocation": "KC"
}
```
### POST /api/logout
Log out and clear session.
**Response:**
```json
{
"status": "success",
"message": "Logged out successfully"
}
```
## Session Data
The session stores:
- `user_id`: Index of user in users.json
- `username`: Username string
- `defaultLocation`: User's default location code
- `currentLocation`: Currently selected location code
- `accessibleLocations`: Array of location codes user can access
## Authentication Decorator
The `@login_required` decorator protects routes:
```python
@app.route('/protected-page')
@login_required
def protected_page():
return render_template('protected.html')
```
The decorator:
1. Checks if user is logged in (has `user_id` in session)
2. Validates user still exists in users.json
3. Checks if user account is still active
4. Redirects to login if any check fails
## Getting Current User in Routes
```python
@app.route('/my-route')
@login_required
def my_route():
user = get_current_user()
username = session.get('username')
current_location = session.get('currentLocation')
# Use user data...
return render_template('page.html')
```
## Location-Based Access Control
Users can only access locations they have permission for:
```python
# In users.json
{
"username": "john_doe",
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": false },
"KC": { "accessible": true },
"BMD": { "accessible": false }
}
}
```
This user can access:
- ✅ Lindsborg (LINDS)
- ✅ KC (KC)
- ❌ Iola (IOLA)
- ❌ BMD (BMD)
## User Interface Components
### Login Page (`/login`)
- Clean, centered login form
- Username and password fields
- Submit button with loading spinner
- Link to User Management page
- Error message display
### Location Selection Page (`/select-location`)
- Shows logged-in username
- Shows default location
- Radio buttons for each accessible location
- Default location is pre-selected
- Continue and Logout buttons
### Main App Header
- User info display: `👤 username | 📍 location`
- Logout button in header
- Positioned in top-right corner
## Security Considerations
### Password Security
- Passwords are hashed using PBKDF2-SHA256
- Hashes are never reversed or displayed
- Hash verification happens server-side only
### Session Security
- Sessions use secure random keys
- Cookies are HTTP-only (not accessible via JavaScript)
- SameSite cookie policy prevents CSRF attacks
- Sessions cleared on logout
### Account Status
- Inactive accounts cannot log in
- If account is deactivated while logged in, next request will log them out
- User must have at least one accessible location
## Testing the Login System
### Test User Creation
1. Go to `/users`
2. Create a test user:
- Username: `testuser`
- Password: `password123`
- Default Location: Lindsborg
- Check "Accessible" for Lindsborg and KC
### Test Login Flow
1. Go to `/` (should redirect to `/login`)
2. Enter credentials: `testuser` / `password123`
3. Click "Sign In"
4. Since user has 2 accessible locations → redirected to `/select-location`
5. Choose a location and click "Continue"
6. Now viewing main Product Finder app
7. See user info in header
8. Click "Logout" to end session
### Test Single Location User
1. Create user with only 1 accessible location
2. Log in
3. Should go directly to main app (skip location selection)
### Test Inactive User
1. Create and log in as a user
2. In User Management, toggle user to "Inactive"
3. Try to log in → Should see "Account is inactive" message
## Troubleshooting
### "Please log in first" on all pages
- Session may have expired
- Server may have restarted (sessions are in-memory)
- Clear browser cookies and log in again
### "User not found" error
- User may have been deleted while logged in
- Log out and log back in
### Can't access certain locations
- Check user's "Accessible" checkboxes in User Management
- User must have at least one accessible location
### Stuck on location selection page
- User must have multiple accessible locations
- If this shouldn't happen, check user's location settings
- Or click "Sign Out" and contact administrator
## Future Enhancements
Potential additions:
- [ ] Remember me checkbox (persistent sessions)
- [ ] Password reset functionality
- [ ] Session timeout after inactivity
- [ ] Login attempt limiting (brute force protection)
- [ ] Two-factor authentication
- [ ] Session management dashboard
- [ ] Location switching without re-login
- [ ] Audit log of login attempts
+264
View File
@@ -0,0 +1,264 @@
# WordPress-Style Permission System - Quick Reference
## ✅ What Was Implemented
The CGW Product Finder now has a complete WordPress-style permission system with:
1. **Permission Checking Functions** (Backend - Python)
- `can_user(permission, location=None)` - Check single permission
- `user_has_any_permission(permissions, location=None)` - Check if user has ANY permission
- `user_has_all_permissions(permissions, location=None)` - Check if user has ALL permissions
- `get_user_permissions(location=None)` - Get all user permissions
- `@permission_required(permission, location=None)` - Route decorator for permission protection
2. **Permission Check Endpoints** (Frontend - API)
- `POST /api/check-permission` - Check if user has specific permission
- `GET /api/user-permissions` - Get all user permissions
3. **Access Denial**
- Beautiful access denied page at `templates/access_denied.html`
- Shows required permission and helpful navigation
4. **Protected Routes**
- `/users` - User management page (requires `manage_users`)
- `/api/users` (GET, POST, PATCH, DELETE) - All user management endpoints protected
5. **User Data Structure**
- Global permissions: `user.permissions`
- Location-specific permissions: `user.locationSettings[LOCATION].permissions`
## 🚀 Quick Start Usage
### Backend (Python)
```python
# Check permission
if can_user('create_quotes'):
# User can create quotes
pass
# Protect a route
@app.route('/admin/reports')
@login_required
@permission_required('view_reports')
def admin_reports():
return render_template('reports.html')
# Check at specific location
if can_user('manage_inventory', location='LINDS'):
# User can manage inventory at Lindsborg
pass
```
### Frontend (JavaScript)
```javascript
// Check single permission
const response = await fetch('/api/check-permission', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ permission: 'create_quotes' })
});
const data = await response.json();
if (data.hasPermission) {
// Show create button
}
// Get all permissions
const response = await fetch('/api/user-permissions');
const data = await response.json();
console.log(data.permissions); // { manage_users: true, create_quotes: true, ... }
```
## 📁 Files Modified/Created
### Created:
- `app/templates/access_denied.html` - Access denial page
- `app/PERMISSIONS_SYSTEM.md` - Comprehensive documentation
### Modified:
- `app/app.py` - Added permission checking functions and protected routes
- `app/data/users.json` - Added permissions to existing users
- `app/data/example_user_structure.json` - Updated with permission examples
## 👥 Current Users & Permissions
### Master (Admin)
- **Password**: Master
- **Location**: KC (access to all locations)
- **Permissions**: Full access
- manage_users ✅
- view_reports ✅
- create_quotes ✅
- approve_quotes ✅
- manage_products ✅
- manage_inventory ✅
### Darlene (Standard User)
- **Password**: Darlene
- **Location**: IOLA (access to LINDS, IOLA, KC)
- **Permissions**: Limited access
- manage_users ✅
- view_reports ✅
- create_quotes ✅
- approve_quotes ❌
- manage_products ❌
- manage_inventory ❌
## 🔒 Permission Hierarchy
```
Check Order:
1. Is user logged in? → If no: return False
2. Is user active? → If no: return False
3. Check global permissions (user.permissions) → If found: return value
4. Check location-specific permissions → If found: return value
5. Default: return False
```
## 🎯 Common Permission Names
Recommended permissions for your system:
**User Management:**
- `manage_users` - Create, edit, delete users (already implemented)
- `view_users` - View user list
- `reset_passwords` - Reset passwords
**Products & Inventory:**
- `manage_products` - Add/edit/delete products
- `view_products` - View product catalog
- `manage_inventory` - Adjust inventory
- `view_inventory` - View inventory
**Quotes & Orders:**
- `create_quotes` - Create quotes
- `view_quotes` - View quotes
- `approve_quotes` - Approve/reject quotes
- `edit_quotes` - Edit quotes
**Reports:**
- `view_reports` - Access reports
- `export_data` - Export data
- `view_analytics` - View analytics
## 🧪 Testing the System
### Test 1: User Management Access
```bash
1. Start Flask server: python app/app.py
2. Login as Master (password: Master)
3. Navigate to /users
4. Should see user management interface ✅
```
### Test 2: Permission Denied
```bash
1. Create a new user without manage_users permission
2. Login as that user
3. Navigate to /users
4. Should see "Access Denied" page ✅
```
### Test 3: API Permission Check
```bash
# In browser console after login:
const response = await fetch('/api/check-permission', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({permission: 'manage_users'})
});
const data = await response.json();
console.log(data.hasPermission); // Should be true for Master
```
### Test 4: Get All Permissions
```bash
# In browser console after login:
const response = await fetch('/api/user-permissions');
const data = await response.json();
console.log(data.permissions); // Should show all user's permissions
```
## 📝 Next Steps
To add permissions to new features:
1. **Define Permission Name**
```python
# Choose a descriptive name like 'create_quotes'
```
2. **Protect Backend Route**
```python
@app.route('/quotes/new')
@login_required
@permission_required('create_quotes')
def new_quote():
return render_template('new_quote.html')
```
3. **Check in Code**
```python
if can_user('create_quotes'):
# Allow quote creation
```
4. **Hide/Show Frontend Elements**
```javascript
const perms = await fetch('/api/user-permissions').then(r => r.json());
if (perms.permissions.create_quotes) {
document.getElementById('createBtn').style.display = 'block';
}
```
5. **Add to User Data**
```json
{
"username": "user",
"permissions": {
"create_quotes": true
}
}
```
## 🛠️ Troubleshooting
**Access Denied even with permission:**
- Check spelling of permission name (case-sensitive)
- Verify user is active in users.json
- Clear browser cookies and re-login
- Check server logs for errors
**Permission check returns False:**
- Ensure user is logged in
- Verify permission exists in users.json
- Check if using correct location context
- Confirm session is valid
**Frontend shows button but backend denies:**
- This is correct! Frontend checks are for UX only
- Backend always enforces permissions
- Never trust client-side permission checks
## 📚 Full Documentation
See `app/PERMISSIONS_SYSTEM.md` for complete documentation including:
- Detailed examples
- Best practices
- Security notes
- Migration guide
- Advanced usage patterns
## 🎉 Summary
You now have a fully functional WordPress-style permission system that allows:
- ✅ Fine-grained access control
- ✅ Global and location-specific permissions
- ✅ Easy permission checks in code
- ✅ Protected routes with decorators
- ✅ Frontend permission checking
- ✅ Beautiful access denied pages
- ✅ Flexible permission inheritance
The system is secure, scalable, and follows WordPress best practices!
+376
View File
@@ -0,0 +1,376 @@
# Permission System Documentation
The CGW Product Finder uses a WordPress-style permission system that allows fine-grained control over what users can do both globally and at specific locations.
## Overview
Permissions can be set at two levels:
1. **Global Permissions**: Apply across all locations (stored in user's `permissions` field)
2. **Location-Specific Permissions**: Apply only at specific locations (stored in `locationSettings[LOCATION].permissions`)
Permission checks follow this hierarchy:
- First checks global permissions
- Then checks location-specific permissions
- Location-specific permissions can override global permissions
- If a permission isn't found anywhere, it defaults to `false`
## User Structure
```json
{
"username": "john_doe",
"password": "pbkdf2:sha256:1000000$...",
"defaultLocation": "LINDS",
"active": true,
"permissions": {
"manage_users": true,
"view_reports": true,
"create_quotes": true
},
"locationSettings": {
"LINDS": {
"accessible": true,
"permissions": {
"manage_inventory": true,
"approve_quotes": true
}
},
"IOLA": {
"accessible": true,
"permissions": {
"manage_inventory": false,
"approve_quotes": false
}
}
}
}
```
## Backend Usage (Python)
### Checking Permissions in Code
```python
from app import can_user
# Check if user has permission at current location
if can_user('create_quotes'):
# User can create quotes
pass
# Check if user has permission at specific location
if can_user('manage_inventory', location='LINDS'):
# User can manage inventory at Lindsborg
pass
# Check only global permissions (ignore location-specific)
if can_user('manage_users', location='global'):
# User has global user management permission
pass
```
### Protecting Routes with Decorators
```python
from app import permission_required, login_required
@app.route('/admin/users')
@login_required
@permission_required('manage_users')
def admin_users():
"""Only users with manage_users permission can access"""
return render_template('admin_users.html')
# Check permission at specific location
@app.route('/inventory/<location>')
@login_required
@permission_required('manage_inventory', location_param='location')
def location_inventory(location):
"""Permission checked for the location in URL parameter"""
return render_template('inventory.html')
```
### Multiple Permission Checks
```python
from app import user_has_any_permission, user_has_all_permissions
# Check if user has ANY of these permissions
if user_has_any_permission(['create_quotes', 'approve_quotes']):
# User can either create OR approve quotes
pass
# Check if user has ALL of these permissions
if user_has_all_permissions(['manage_users', 'view_reports']):
# User has both permissions
pass
```
### Getting All User Permissions
```python
from app import get_user_permissions
# Get all permissions (global + current location)
permissions = get_user_permissions()
# Returns: {'manage_users': True, 'create_quotes': True, ...}
# Get permissions for specific location
permissions = get_user_permissions(location='LINDS')
# Get only global permissions
permissions = get_user_permissions(location='global')
```
## Frontend Usage (JavaScript)
### Checking Single Permission
```javascript
async function checkPermission(permissionName, location = null) {
try {
const response = await fetch('/api/check-permission', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
permission: permissionName,
location: location // Optional
})
});
const data = await response.json();
return data.hasPermission;
} catch (error) {
console.error('Error checking permission:', error);
return false;
}
}
// Usage
if (await checkPermission('create_quotes')) {
// Show create quote button
document.getElementById('createQuoteBtn').style.display = 'block';
}
```
### Getting All User Permissions
```javascript
async function getUserPermissions(location = null) {
try {
const url = location
? `/api/user-permissions?location=${location}`
: '/api/user-permissions';
const response = await fetch(url);
const data = await response.json();
return data.permissions;
} catch (error) {
console.error('Error fetching permissions:', error);
return {};
}
}
// Usage
const permissions = await getUserPermissions();
if (permissions.manage_users) {
// Show admin menu
}
```
### Show/Hide Elements Based on Permissions
```javascript
async function initializePermissions() {
const permissions = await getUserPermissions();
// Show/hide elements
document.querySelectorAll('[data-permission]').forEach(element => {
const requiredPermission = element.dataset.permission;
if (!permissions[requiredPermission]) {
element.style.display = 'none';
}
});
}
// In HTML:
// <button data-permission="create_quotes">Create Quote</button>
// <div data-permission="manage_users">Admin Panel</div>
```
## Common Permissions
Here are some suggested permission names for the Product Finder:
### User Management
- `manage_users` - Create, edit, delete users
- `view_users` - View user list
- `reset_passwords` - Reset other users' passwords
### Product & Inventory
- `manage_products` - Add/edit/delete products
- `view_products` - View product catalog
- `manage_inventory` - Adjust inventory levels
- `view_inventory` - View inventory levels
### Quotes & Orders
- `create_quotes` - Create new quotes
- `view_quotes` - View quotes
- `approve_quotes` - Approve/reject quotes
- `edit_quotes` - Edit existing quotes
- `delete_quotes` - Delete quotes
### Reports & Data
- `view_reports` - Access reporting tools
- `export_data` - Export data to CSV/Excel
- `view_analytics` - View analytics dashboard
### System Settings
- `manage_settings` - Change system settings
- `manage_locations` - Add/edit location settings
- `view_logs` - View system logs
## Permission Flow Examples
### Example 1: Creating a Quote
```python
@app.route('/api/quotes', methods=['POST'])
@login_required
@permission_required('create_quotes')
def create_quote():
# User needs create_quotes permission at their current location
data = request.json
# Create quote logic...
return jsonify({'status': 'success'})
```
### Example 2: Approving Quotes (Location-Specific)
```python
@app.route('/api/quotes/<quote_id>/approve', methods=['POST'])
@login_required
def approve_quote(quote_id):
# Check permission at the quote's location
quote = get_quote(quote_id)
if not can_user('approve_quotes', location=quote['location']):
return render_template('access_denied.html',
required_permission='approve_quotes'), 403
# Approve quote logic...
return jsonify({'status': 'success'})
```
### Example 3: Multi-Location Access
```python
@app.route('/api/inventory/transfer', methods=['POST'])
@login_required
def transfer_inventory():
data = request.json
from_location = data['from_location']
to_location = data['to_location']
# User must have manage_inventory at BOTH locations
if not user_has_all_permissions(['manage_inventory'], location=from_location):
return jsonify({'error': 'No permission at source location'}), 403
if not user_has_all_permissions(['manage_inventory'], location=to_location):
return jsonify({'error': 'No permission at destination location'}), 403
# Transfer logic...
return jsonify({'status': 'success'})
```
## Access Denied Page
When a user lacks permission, they see an access denied page that shows:
- Clear "Access Denied" message
- The specific permission that was required
- Options to go back or return home
- Contact information for requesting access
## Best Practices
1. **Be Specific**: Use descriptive permission names like `create_quotes` instead of `quotes`
2. **Granular Control**: Separate permissions (create, view, edit, delete) rather than one "manage" permission
3. **Check Early**: Check permissions at route level with decorators when possible
4. **Check Often**: Re-check permissions before critical operations, not just at page load
5. **Fail Secure**: Default to denying access if permission isn't explicitly granted
6. **Location Context**: Always consider whether a permission should be global or location-specific
7. **UI Feedback**: Hide/disable UI elements users can't use based on permissions
8. **Clear Errors**: Show helpful error messages when permission is denied
## Adding New Permissions
To add a new permission:
1. **Define the permission** in your data structure (add to user's `permissions` or `locationSettings[LOCATION].permissions`)
2. **Protect routes** with `@permission_required('new_permission')`
3. **Check in code** with `can_user('new_permission')`
4. **Update frontend** to show/hide elements based on permission
5. **Document** the permission in this file
## Troubleshooting
### Permission check returns False but user should have access
- Check if permission is spelled correctly (case-sensitive)
- Verify user is logged in (`session['user_id']` exists)
- Check user's `active` status
- Verify permission exists in either global or location-specific permissions
- Check if using correct location (current vs specific vs global)
### Access denied page shows even for users with permission
- Ensure decorators are in correct order: `@login_required` before `@permission_required`
- Check that permission name matches exactly
- Verify user data was saved correctly in users.json
- Clear browser cache/cookies if session is stale
### Frontend shows elements but backend denies access
- Frontend permission checks are for UX only - always enforce in backend
- Make sure frontend is checking the same permission name
- Ensure frontend is checking at the same location context
## Security Notes
- **Never trust frontend permission checks** - they're for UI only
- **Always validate permissions on the backend** before performing operations
- **Session security** - permissions are loaded from session, which is server-side
- **Password hashing** - uses PBKDF2-SHA256 with 1M iterations
- **HTTP-only cookies** - session cookies cannot be accessed by JavaScript
- **Permission inheritance** - location-specific permissions override global ones
## Migration Guide
If you have existing users without permissions, you can add default permissions:
```python
import json
def add_default_permissions():
with open('data/users.json', 'r') as f:
users = json.load(f)
for user in users:
# Add global permissions if missing
if 'permissions' not in user:
user['permissions'] = {
'view_products': True,
'create_quotes': True,
'manage_users': False # Admin only
}
# Add location permissions if missing
for location in user.get('locationSettings', {}):
if 'permissions' not in user['locationSettings'][location]:
user['locationSettings'][location]['permissions'] = {
'manage_inventory': False,
'approve_quotes': False
}
with open('data/users.json', 'w') as f:
json.dump(users, f, indent=2)
```
+334
View File
@@ -0,0 +1,334 @@
# /product-finder Deployment Fix Guide
## 🔴 Problem
The application works locally at `http://localhost:8080/` but fails on the server at `https://columbiawindows.com/product-finder/`.
The redirect from `/product-finder``/product-finder/login` works, but then the login page or subsequent routes fail.
## ✅ Solution Applied
Three critical changes were made to fix subdirectory deployment:
### 1. **Added PrefixMiddleware to app.py**
This middleware tells Flask about the `/product-finder` prefix by setting `SCRIPT_NAME` in the WSGI environment:
```python
class PrefixMiddleware:
"""Middleware to handle subdirectory deployments"""
def __init__(self, app, prefix=''):
self.app = app
self.prefix = prefix.rstrip('/')
def __call__(self, environ, start_response):
if self.prefix and self.prefix != '/':
path = environ.get('PATH_INFO', '')
script_name = environ.get('SCRIPT_NAME', '')
if not script_name.startswith(self.prefix):
environ['SCRIPT_NAME'] = self.prefix + script_name
if path.startswith(self.prefix):
environ['PATH_INFO'] = path[len(self.prefix):]
return self.app(environ, start_response)
```
This is automatically applied when `APPLICATION_ROOT` is set.
### 2. **Updated passenger_wsgi.py**
Sets the `APPLICATION_ROOT` environment variable before importing the app:
```python
if 'APPLICATION_ROOT' not in os.environ:
os.environ['APPLICATION_ROOT'] = '/product-finder'
```
### 3. **Updated config.py**
Production configuration now defaults to `/product-finder`:
```python
class ProductionConfig(Config):
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/product-finder')
```
Development still uses `/` for local testing.
## 📤 Files to Upload
Upload these updated files to your server:
1. **`app/app.py`** - Contains PrefixMiddleware
2. **`app/passenger_wsgi.py`** - Sets APPLICATION_ROOT env var
3. **`app/config.py`** - Production defaults to /product-finder
4. **`app/.htaccess`** - Apache configuration (new file)
5. **All template files** - Already updated with url_for() and BASE_URL
## 🔧 Server Configuration
### Option A: Using .htaccess (Recommended)
The `.htaccess` file is already configured for `/product-finder`. Upload it to your `app/` folder on the server.
**Important:** Update these lines in `.htaccess`:
```apache
PassengerAppRoot /home/USERNAME/public_html/product-finder
PassengerPython /usr/bin/python3 # Update to your Python path
```
### Option B: Apache Virtual Host Configuration
If you have access to Apache config, add this to your virtual host:
```apache
<Directory "/home/USERNAME/public_html/product-finder">
SetEnv APPLICATION_ROOT /product-finder
PassengerEnabled on
PassengerAppRoot /home/USERNAME/public_html/product-finder
PassengerPython /usr/bin/python3
Allow from all
Options -MultiViews
</Directory>
```
## 🚀 Deployment Steps
1. **Backup Current Installation**
```bash
mv product-finder product-finder.backup
```
2. **Upload Updated Files**
- Upload entire `app/` folder to server
- Or just upload the 5 changed files listed above
3. **Restart Passenger**
```bash
# In product-finder folder
mkdir -p tmp
touch tmp/restart.txt
```
4. **Test the Application**
- Go to: `https://columbiawindows.com/product-finder/`
- Should redirect to: `https://columbiawindows.com/product-finder/login`
- Login page should load with all CSS/JS
- Login should work and redirect properly
- All routes should work: `/product-finder/users`, etc.
## 🐛 Troubleshooting
### Issue: Still getting 404 on login page
**Check server error logs:**
```bash
tail -f ~/logs/error_log # or wherever your error logs are
```
**Verify APPLICATION_ROOT is set:**
Add this test route to app.py temporarily:
```python
@app.route('/debug-config')
def debug_config():
return jsonify({
'APPLICATION_ROOT': app.config.get('APPLICATION_ROOT'),
'script_root': request.script_root,
'url_root': request.url_root,
'base_url': request.base_url
})
```
Then visit: `https://columbiawindows.com/product-finder/debug-config`
**Expected response:**
```json
{
"APPLICATION_ROOT": "/product-finder",
"script_root": "/product-finder",
"url_root": "https://columbiawindows.com/product-finder/",
"base_url": "https://columbiawindows.com/product-finder/debug-config"
}
```
### Issue: CSS/JS files not loading
**Check that routes are working:**
- Visit: `https://columbiawindows.com/product-finder/css/styles.css`
- Should return CSS file, not 404
**Check .htaccess MIME types:**
Ensure these lines are in `.htaccess`:
```apache
AddType text/css .css
AddType application/javascript .js
AddType application/json .json
```
### Issue: Login works but redirects are wrong
**Check redirect code in templates:**
All JavaScript should use `BASE_URL`:
```javascript
const BASE_URL = '{{ base_url }}';
window.location.href = BASE_URL + '/login';
```
All Python redirects should use `url_for()`:
```python
return redirect(url_for('login_page'))
```
### Issue: Works locally, fails on server
**Verify environment:**
```bash
# SSH to server
cd ~/public_html/product-finder
python3 -c "import os; print(os.environ.get('APPLICATION_ROOT', 'NOT SET'))"
```
Should print: `/product-finder`
**Check Passenger is using correct Python:**
```bash
which python3
# Use this path in PassengerPython directive
```
### Issue: Sessions not persisting
**Check SECRET_KEY:**
```python
# In config.py, production should have a fixed SECRET_KEY
SECRET_KEY = 'your-fixed-secret-key-here' # Don't use secrets.token_hex() in production
```
**Check cookie settings:**
Session cookies need to work with the subdirectory path.
### Issue: API calls return 404
**Check browser console:**
Press F12, go to Network tab, and check the actual URLs being called.
**Should see:**
```
https://columbiawindows.com/product-finder/api/login
https://columbiawindows.com/product-finder/api/session
```
**If you see:**
```
https://columbiawindows.com/api/login ❌ Missing prefix
```
Then `BASE_URL` is not set correctly in template.
## ✅ Verification Checklist
After deployment, test these in order:
- [ ] Visit `https://columbiawindows.com/product-finder/`
- Should redirect to `/product-finder/login` ✓
- [ ] Login page loads
- CSS styled correctly ✓
- No 404s in browser console ✓
- [ ] Login with Master/Master
- Should redirect to `/product-finder/select-location` ✓
- [ ] Select a location
- Should redirect to `/product-finder/` ✓
- [ ] User info shows in header ✓
- [ ] Click "User Management" (if Master user)
- Should go to `/product-finder/users` ✓
- [ ] Logout
- Should return to `/product-finder/login` ✓
## 📝 Key Points
1. **The middleware is critical** - It tells Flask about the `/product-finder` prefix
2. **passenger_wsgi.py sets the env var** - Before importing the app
3. **All templates use BASE_URL** - For JavaScript fetch calls
4. **All routes use url_for()** - For Python redirects
5. **Production config defaults to /product-finder** - Development stays at /
## 🔄 Rolling Back
If something goes wrong:
```bash
# Remove new files
rm -rf product-finder
# Restore backup
mv product-finder.backup product-finder
# Restart Passenger
touch product-finder/tmp/restart.txt
```
## 📞 Still Having Issues?
Run this diagnostic script on the server:
```python
# Save as test_deployment.py in product-finder folder
import os
import sys
print("=" * 60)
print("DEPLOYMENT DIAGNOSTIC")
print("=" * 60)
print(f"Python Version: {sys.version}")
print(f"Current Directory: {os.getcwd()}")
print(f"APPLICATION_ROOT env: {os.environ.get('APPLICATION_ROOT', 'NOT SET')}")
print()
try:
os.environ['APPLICATION_ROOT'] = '/product-finder'
from app import app
print("✓ App imported successfully")
print(f"APPLICATION_ROOT config: {app.config.get('APPLICATION_ROOT')}")
print(f"Middleware applied: {'PrefixMiddleware' in str(type(app.wsgi_app))}")
except Exception as e:
print(f"✗ Error importing app: {e}")
import traceback
traceback.print_exc()
```
Run with: `python3 test_deployment.py`
## 🎯 Expected Behavior
**Before these changes:**
- Redirect works: `/product-finder` → `/product-finder/login` ✓
- Login page loads BUT Flask doesn't know about `/product-finder` prefix
- All `url_for()` calls generate `/login` instead of `/product-finder/login` ❌
- Result: 404 errors on subpages
**After these changes:**
- Flask knows it's at `/product-finder` via middleware ✓
- All `url_for()` generates `/product-finder/login` ✓
- All templates use `BASE_URL = '/product-finder'` ✓
- Result: Everything works ✓
## 🆘 Quick Fix Checklist
If deployed and not working:
1. [ ] Uploaded `app/app.py` with PrefixMiddleware?
2. [ ] Uploaded `app/passenger_wsgi.py` with env var setting?
3. [ ] Uploaded `app/.htaccess` with SetEnv directive?
4. [ ] Ran `touch tmp/restart.txt` to restart Passenger?
5. [ ] Checked error logs for Python errors?
6. [ ] Tested `/product-finder/debug-config` route?
7. [ ] Verified cookies are being set (F12 > Application > Cookies)?
If all checked and still failing, check server error logs for the actual Python error.
+47
View File
@@ -0,0 +1,47 @@
# Product Finder Quiz - Quick Start Guide
## Installation and Running
### Step 1: Install Python
Make sure Python 3.8+ is installed:
```bash
python --version
```
### Step 2: Install Dependencies
```bash
pip install -r requirements.txt
```
### Step 3: Run the Application
```bash
python app.py
```
### Step 4: Access the Application
Open your browser and go to:
- **Quiz Page**: http://localhost:8080/quiz
- **Main Page**: http://localhost:8080/
## Stopping the Application
Press `Ctrl+C` in the terminal to stop the server.
## Troubleshooting
**"Port already in use" error:**
Change the port in `app.py`:
```python
app.run(debug=True, host='0.0.0.0', port=8081)
```
**Missing modules:**
```bash
pip install -r requirements.txt
```
**Templates not found:**
Make sure the `templates` folder contains `index2.html` and other HTML files.
## For Production Deployment
See the detailed [README.md](README.md) file for deployment instructions.
+171
View File
@@ -0,0 +1,171 @@
# Product Finder Quiz - Flask Web Application
A Python Flask web application for product selection through an interactive quiz interface.
## Features
- Interactive quiz with button-based and form-based questions
- Dynamic conditional logic based on user responses
- Product recommendations with images
- Responsive design
- Memory storage for user answers
## Installation
### Prerequisites
- Python 3.8 or higher
- pip (Python package installer)
### Setup
1. **Install dependencies:**
```bash
pip install -r requirements.txt
```
2. **Create environment file (optional):**
```bash
copy .env.example .env
```
Edit `.env` with your configuration.
3. **Run the application:**
```bash
python app.py
```
4. **Access the application:**
Open your browser and navigate to:
- Main page: `http://localhost:8080/`
- Quiz page: `http://localhost:8080/quiz`
## Project Structure
```
project/
├── app.py # Main Flask application
├── config.py # Configuration settings
├── wsgi.py # WSGI entry point for production
├── requirements.txt # Python dependencies
├── templates/ # HTML templates
│ └── index2.html # Quiz page
├── css/ # Stylesheets
│ └── styles.css # Main stylesheet
├── js/ # JavaScript files
│ └── script.js # Quiz logic and data
└── images/ # Product images (optional)
```
## Deployment
### Using Gunicorn (Production)
1. **Install Gunicorn:**
```bash
pip install gunicorn
```
2. **Run with Gunicorn:**
```bash
gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app
```
### Deployment on Web Panels
#### cPanel/Plesk
1. Upload all files to your hosting directory
2. Install Python dependencies via terminal or SSH
3. Configure the web server to use Python WSGI
4. Set the application entry point to `wsgi:app`
#### PythonAnywhere
1. Upload files or clone from repository
2. Create a new web app (Flask)
3. Set WSGI file path to `wsgi.py`
4. Install requirements in virtual environment
5. Reload the web app
#### Heroku
1. Create `Procfile`:
```
web: gunicorn wsgi:app
```
2. Deploy using Git:
```bash
git init
git add .
git commit -m "Initial commit"
heroku create
git push heroku main
```
#### Docker
1. Create `Dockerfile`:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "wsgi:app"]
```
2. Build and run:
```bash
docker build -t product-finder .
docker run -p 8080:8080 product-finder
```
## Configuration
Edit `config.py` to modify application settings:
- `DEBUG`: Enable/disable debug mode
- `SECRET_KEY`: Set a secure secret key for production
- Add database URLs, API keys, etc.
## API Endpoints
- `GET /` - Main landing page
- `GET /quiz` - Product finder quiz
- `POST /api/save-selection` - Save user selections (for future use)
- `GET /api/get-products` - Get product data (for future use)
## Customization
### Adding New Products
Edit `js/script.js` and add new product entries to the `questionData` object.
### Styling
Modify `css/styles.css` to change colors, layouts, and appearance.
### Questions and Logic
Update the quiz flow in `js/script.js` by modifying the question structure and conditional logic.
## Development
Run in development mode with auto-reload:
```bash
python app.py
```
The application will be available at `http://localhost:8080`
## Production Checklist
- [ ] Set `DEBUG = False` in config
- [ ] Use a strong `SECRET_KEY`
- [ ] Configure proper database (if needed)
- [ ] Set up SSL/HTTPS
- [ ] Configure proper logging
- [ ] Set up error monitoring
- [ ] Enable CORS if needed
- [ ] Configure environment variables
- [ ] Test all routes and functionality
## License
Proprietary - All rights reserved
## Support
For issues and questions, contact your development team.
+791
View File
@@ -0,0 +1,791 @@
# Required Code Changes for Conditional Navigation
## Overview
To support conditional material/color questions and dynamic navigation, the following changes are needed in your application code.
---
## 1. JavaScript Changes (`js/script.js`)
### Current State
- Loads `questions.json` statically
- Has hardcoded conditional for `q-material-conditional`
- No product data loading
### Required Changes
#### A. Load Multiple Data Files
```javascript
// At the top of script.js - modify loadQuestionData() to load both files
let questionData = {};
let productData = [];
let accessoryData = [];
let bitwiseData = {};
let accumulatedBitValue = 0;
function init() {
// Load all data files
Promise.all([
fetch('data/navigation.json').then(r => r.json()),
fetch('data/products.json').then(r => r.json()),
fetch('data/accessories.json').then(r => r.json()),
fetch('data/product_bitwise.json').then(r => r.json())
])
.then(([questions, products, accessories, bitwise]) => {
questionData = questions;
productData = products;
accessoryData = accessories;
// Index bitwise data by product code
bitwiseData = {};
bitwise.forEach(item => {
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
});
// Check if there's state in URL
initFromURL();
// If no URL state, start normally
if (!window.location.search) {
loadContent('start');
}
})
.catch(error => {
console.error('Error loading data:', error);
// Show error message
});
}
```
#### B. Add Dynamic Next Resolution Function
```javascript
// Add this new function to handle conditional navigation
function resolveNextQuestion(currentKey, selectedAnswer, answerObject) {
// Get the current accumulated filters
const currentFilters = buildFilterFromAnswers();
// Add the new filter from this answer
if (answerObject.filter) {
Object.assign(currentFilters, answerObject.filter);
}
// Check if next question in chain should be shown
const nextKey = answerObject.next;
const nextQuestion = questionData[nextKey];
// If next question has conditional flag, evaluate it
if (nextQuestion && nextQuestion.conditional) {
return evaluateConditional(nextQuestion, currentFilters);
}
return nextKey;
}
// Evaluate if a conditional question should be shown
function evaluateConditional(question, filters) {
if (question.conditional.type === 'material') {
// Check if products matching current filters have multiple materials
const matchingProducts = filterProducts(filters);
const materials = getAvailableMaterials(matchingProducts);
if (materials.length <= 1) {
// Skip this question, auto-apply material if exists
if (materials.length === 1) {
userAnswers[question.id + '.material'] = materials[0];
}
// Return the fallback next
return question.conditional.fallbackNext || question.next;
}
}
if (question.conditional.type === 'color') {
// Similar logic for colors
const matchingProducts = filterProducts(filters);
const colors = getAvailableColors(matchingProducts);
if (colors.length <= 1) {
if (colors.length === 1) {
userAnswers[question.id + '.color'] = colors[0];
}
return question.conditional.fallbackNext || question.next;
}
}
// Show the question
return question.id;
}
// Build current filter object from user answers
function buildFilterFromAnswers() {
const filters = {};
// Extract filter info from answers
for (const [key, value] of Object.entries(userAnswers)) {
if (key === 'start') {
filters.baseType = value;
} else if (key.includes('type')) {
filters.subType = value;
} else if (key.includes('material')) {
filters.material = value;
} else if (key.includes('color')) {
filters.color = value;
}
}
return filters;
}
// Filter products based on criteria
function filterProducts(filters) {
return productData.filter(product => {
if (filters.baseType && product.baseType !== filters.baseType) {
return false;
}
if (filters.subType) {
const subType = product.subType.door || product.subType.window;
if (subType !== filters.subType) {
return false;
}
}
if (filters.material && !product.materials.includes(filters.material)) {
return false;
}
if (filters.color && !product.colors.includes(filters.color)) {
return false;
}
return true;
});
}
// Get available materials from product set
function getAvailableMaterials(products) {
const materials = new Set();
products.forEach(p => {
p.materials.forEach(m => materials.add(m));
});
return Array.from(materials);
}
// Get available colors from product set
function getAvailableColors(products) {
const colors = new Set();
products.forEach(p => {
p.colors.forEach(c => colors.add(c));
});
return Array.from(colors);
}
```
#### C. Update handleAnswer Function
```javascript
// Replace the existing handleAnswer function
function handleAnswer(currentKey, answerValue, answerIndex) {
const currentQuestion = questionData[currentKey];
const answerObject = currentQuestion.answers[answerIndex];
// Store the answer
userAnswers[currentKey] = answerValue;
// Update bit value based on selection
updateBitValue(answerObject);
// Resolve the next question (handles conditionals)
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
history.push(nextKey);
loadContent(nextKey);
}
```
#### D. Update renderQuestion to Pass Answer Index
```javascript
// In renderQuestion function, change the button onclick
// FROM:
// onclick="handleAnswer('${currentKey}', '${answer.caption}', '${answer.next}')"
// TO:
data.answers.forEach((answer, index) => {
html += `
<button class="answer-button" onclick="handleAnswer('${currentKey}', '${answer.caption}', ${index})">
<div class="answer-image">${answer.image}</div>
<div class="answer-caption">${answer.caption}</div>
</button>
`;
});
```
#### D2. Add URL State Management
```javascript
// Track accumulated bit value from user selections
let accumulatedBitValue = 0;
// Load bitwise helper data
let bitwiseData = {};
function loadBitwiseData() {
return fetch('data/product_bitwise.json')
.then(r => r.json())
.then(data => {
bitwiseData = {};
data.forEach(item => {
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
});
return bitwiseData;
});
}
// Initialize from URL on page load
function initFromURL() {
const params = new URLSearchParams(window.location.search);
// Check for bit value in URL
const bitValue = params.get('b');
if (bitValue) {
accumulatedBitValue = parseInt(bitValue, 10);
// Restore state based on bit value
restoreStateFromBitValue(accumulatedBitValue);
}
// Check for product code in URL
const productCode = params.get('p');
if (productCode) {
showProductByCode(productCode);
}
}
// Update URL with current state
function updateURL() {
const params = new URLSearchParams();
if (accumulatedBitValue > 0) {
params.set('b', accumulatedBitValue);
}
// Get current question
if (history.length > 0) {
const currentKey = history[history.length - 1];
params.set('q', currentKey);
}
// Update URL without reloading page
const newURL = window.location.pathname + '?' + params.toString();
window.history.replaceState({ bitValue: accumulatedBitValue }, '', newURL);
}
// Update bit value based on user selection
function updateBitValue(answerObject) {
if (answerObject.filter) {
// Add bits based on filter
if (answerObject.filter.baseType === 'Door') {
accumulatedBitValue |= 1; // Bit 0
} else if (answerObject.filter.baseType === 'Window') {
accumulatedBitValue |= 2; // Bit 1
}
if (answerObject.filter.material === 'Aluminum') {
accumulatedBitValue |= 16; // Bit 4
} else if (answerObject.filter.material === 'Vinyl') {
accumulatedBitValue |= 32; // Bit 5
}
// Add color bits
const colorBits = {
'Black': 64,
'White': 128,
'Bronze': 256,
'Tan': 512,
'Mill': 1024,
'Sandstone': 2048
};
if (answerObject.filter.color && colorBits[answerObject.filter.color]) {
accumulatedBitValue |= colorBits[answerObject.filter.color];
}
// Add subtype bits (based on bitwise_legend.json)
const subtypeBits = {
'Patio Door': 4096,
'Primary Window': 8192,
'Storm Door': 16384,
'Storm Window': 32768
};
if (answerObject.filter.subType && subtypeBits[answerObject.filter.subType]) {
accumulatedBitValue |= subtypeBits[answerObject.filter.subType];
}
}
updateURL();
}
// Restore state from bit value
function restoreStateFromBitValue(bitValue) {
// Decode bit value back to user selections
const selections = {};
if (bitValue & 1) selections.baseType = 'Door';
if (bitValue & 2) selections.baseType = 'Window';
if (bitValue & 16) selections.material = 'Aluminum';
if (bitValue & 32) selections.material = 'Vinyl';
const colors = ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone'];
const colorBits = [64, 128, 256, 512, 1024, 2048];
colors.forEach((color, idx) => {
if (bitValue & colorBits[idx]) {
selections.color = color;
}
});
// Store in userAnswers
if (selections.baseType) userAnswers['start'] = selections.baseType;
if (selections.material) userAnswers['q-material.material'] = selections.material;
if (selections.color) userAnswers['q-color.color'] = selections.color;
// Navigate to appropriate question from URL param
const params = new URLSearchParams(window.location.search);
const questionKey = params.get('q') || 'start';
loadContent(questionKey);
}
// Share current state
function shareCurrentState() {
const url = window.location.href;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(() => {
alert('Link copied to clipboard! Share this link to return to this exact state.');
});
} else {
// Fallback: show URL
prompt('Copy this URL to share:', url);
}
}
// Add share button to breadcrumb
function updateBreadcrumbWithShare() {
const breadcrumbDiv = document.getElementById('breadcrumb');
const shareButton = `
<button onclick="shareCurrentState()"
style="float: right; padding: 5px 10px; cursor: pointer;">
🔗 Share
</button>
`;
breadcrumbDiv.innerHTML += shareButton;
}
```
#### E. Add Product Results Page
```javascript
// Add new function to show filtered products
function showProductResults(filters) {
const matchingProducts = filterProducts(filters);
const contentDiv = document.getElementById('content');
if (matchingProducts.length === 0) {
contentDiv.innerHTML = `
<div class="result-container">
<div class="result-title">No Products Found</div>
<div class="result-details">
No products match your specifications. Please try different options.
</div>
<button class="back-button" onclick="goBack()">← Go Back</button>
</div>
`;
return;
}
let html = `
<div class="result-container">
<div class="result-title">Found ${matchingProducts.length} Product(s)</div>
<div class="products-grid">
`;
matchingProducts.forEach(product => {
// Get compatible accessories
const compatibleAccessories = accessoryData.filter(acc =>
product.compatibleAccessories.includes(acc.id)
);
html += `
<div class="product-card" onclick="showProductDetail('${product.productCode}')">
<h3>${product.description}</h3>
<p><strong>Code:</strong> ${product.productCode}</p>
<p><strong>Materials:</strong> ${product.materials.join(', ')}</p>
<p><strong>Colors:</strong> ${product.colors.join(', ')}</p>
${compatibleAccessories.length > 0 ? `
<p><strong>Available Options:</strong></p>
<ul>
${compatibleAccessories.map(acc =>
`<li>${acc.description}</li>`
).join('')}
</ul>
` : ''}
</div>
`;
});
html += `
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Go Back</button>
<button class="back-button" onclick="startOver()">Start Over</button>
<button class="back-button" onclick="shareCurrentState()">🔗 Share Results</button>
</div>
</div>
`;
contentDiv.innerHTML = html;
}
// Show individual product detail with URL update
function showProductDetail(productCode) {
const product = productData.find(p => p.productCode === productCode);
if (!product) return;
// Update URL with product code
const params = new URLSearchParams();
params.set('p', productCode);
if (accumulatedBitValue > 0) {
params.set('b', accumulatedBitValue);
}
window.history.pushState({ productCode }, '', '?' + params.toString());
// Get compatible accessories
const compatibleAccessories = accessoryData.filter(acc =>
product.compatibleAccessories.includes(acc.id)
);
const contentDiv = document.getElementById('content');
let html = `
<div class="result-container">
<div class="result-title">${product.description}</div>
<div class="result-content">
<div class="result-details">
<p><strong>Product Code:</strong> ${product.productCode}</p>
<p><strong>Category:</strong> ${product.category}</p>
<p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
<p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
<p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
${compatibleAccessories.length > 0 ? `
<h3 style="margin-top: 20px;">Compatible Accessories</h3>
<div class="accessories-list">
${compatibleAccessories.map(acc => `
<div class="accessory-item">
<strong>${acc.description}</strong><br>
<small>Code: ${acc.accessoryCode}</small><br>
<small>Materials: ${acc.materials.join(', ')}</small>
</div>
`).join('')}
</div>
` : ''}
</div>
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Back to Results</button>
<button class="back-button" onclick="startOver()">Start Over</button>
<button class="back-button" onclick="shareCurrentState()">🔗 Share Product</button>
</div>
</div>
`;
contentDiv.innerHTML = html;
}
// Show product by code (from URL)
function showProductByCode(productCode) {
const product = productData.find(p => p.productCode === productCode);
if (product) {
showProductDetail(productCode);
}
}
```
---
## 2. JSON Structure Changes (`data/navigation.json`)
### Add Conditional Metadata to Questions
Questions that might be skipped need a `conditional` property:
```json
{
"q-material-storm-door": {
"type": "question",
"inputType": "button",
"title": "Select Material",
"subtitle": "Choose your preferred material",
"conditional": {
"type": "material",
"fallbackNext": "q-dimensions",
"autoApply": true
},
"answers": [
{
"caption": "Aluminum",
"next": "q-color",
"filter": { "material": "Aluminum" }
},
{
"caption": "Vinyl",
"next": "q-color",
"filter": { "material": "Vinyl" }
}
]
}
}
```
### Add Filter Properties to Answers
Each answer should include filter criteria:
```json
{
"caption": "Storm Door",
"image": "🚪",
"next": "q-material-storm-door",
"filter": {
"baseType": "Door",
"subType": "Storm Door"
}
}
```
---
## 3. CSS Changes (`css/styles.css`)
Add styles for product grid:
```css
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
padding: 20px;
}
.product-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.product-card h3 {
margin-top: 0;
color: #333;
font-size: 1.1em;
}
.product-card p {
margin: 10px 0;
line-height: 1.6;
}
.product-card ul {
list-style-position: inside;
padding-left: 0;
}
```
---
## 4. Python Changes (OPTIONAL)
If you want server-side filtering:
```python
# Add to app.py
import json
import csv
@app.route('/api/filter-products', methods=['POST'])
def filter_products():
"""Filter products based on criteria"""
filters = request.json
# Load products
with open('data/products.json', 'r') as f:
products = json.load(f)
# Apply filters
filtered = []
for product in products:
if filters.get('baseType') and product['baseType'] != filters['baseType']:
continue
if filters.get('subType'):
sub = product['subType'].get('door') or product['subType'].get('window')
if sub != filters['subType']:
continue
if filters.get('material') and filters['material'] not in product['materials']:
continue
if filters.get('color') and filters['color'] not in product['colors']:
continue
filtered.append(product)
return jsonify({
'status': 'success',
'count': len(filtered),
'products': filtered
})
```
---
## 5. CSV Parsing Script Changes
Create a Python script to generate the JSON files:
```python
# create_json_files.py
import csv
import json
from collections import defaultdict
def parse_products_csv(csv_path):
"""Parse products.csv and generate three JSON files"""
products = []
accessories = []
nav_data = {
"start": {
"type": "question",
"inputType": "button",
"title": "What are you looking for?",
"subtitle": "Select the product category",
"answers": []
}
}
# Track unique values for navigation
base_types = set()
door_subtypes = defaultdict(int) # Track material diversity per subtype
window_subtypes = defaultdict(int)
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f, skipinitialspace=True)
for row in reader:
# Skip discontinued
if row['DISCONT'].upper() == 'TRUE':
continue
# Parse materials
materials = []
if row['Aluminum'].upper() == 'TRUE':
materials.append('Aluminum')
if row['Vinyl'].upper() == 'TRUE':
materials.append('Vinyl')
# Parse colors
colors = []
for color in ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone']:
if row.get(color, '').upper() == 'TRUE':
colors.append(color)
# Determine if accessory
is_accessory = row['Accessory\\nYes'].upper() == 'TRUE'
# Build sub-type object
sub_type = {
"door": row['Sub-type\\nDoor'].strip() if row['Sub-type\\nDoor'] else None,
"window": row['Sub-type\\nWindow'].strip() if row['Sub-type\\nWindow'] else None
}
# Common data
item_data = {
"id": row['PROD_CODE'],
"productCode": row['PROD_CODE'],
"category": row['CATEGORY'],
"description": row['DESCRIPTION'],
"discontinued": False,
"location": row['LOC_CODE'],
"baseType": row['Base\\nType'].strip() if row['Base\\nType'] else None,
"subType": sub_type,
"materials": materials,
"colors": colors
}
if is_accessory:
# Add to accessories
accessories.append({
**item_data,
"compatibilityRules": {
"type": "subType",
"subTypeDoor": sub_type['door'],
"subTypeWindow": sub_type['window'],
# ... more rules
}
})
else:
# Add to products
products.append({
**item_data,
"isAccessory": False,
"compatibleAccessories": []
})
# Track for navigation
if item_data['baseType']:
base_types.add(item_data['baseType'])
# Build navigation structure
# ... (logic to create navigation.json based on analysis)
# Write JSON files
with open('data/products.json', 'w') as f:
json.dump(products, f, indent=2)
with open('data/accessories.json', 'w') as f:
json.dump(accessories, f, indent=2)
with open('data/navigation.json', 'w') as f:
json.dump(nav_data, f, indent=2)
if __name__ == '__main__':
parse_products_csv('data/products.csv')
print("JSON files generated successfully!")
```
---
## Implementation Checklist
- [ ] Update `js/script.js` with conditional navigation logic
- [ ] Create parsing script to generate JSON files from CSV
- [ ] Run parsing script to create `navigation.json`, `products.json`, `accessories.json`
- [ ] Update CSS for product display
- [ ] Test navigation flow with different product types
- [ ] Verify material questions are skipped when appropriate
- [ ] Verify color questions are skipped when appropriate
- [ ] Test product filtering and results display
- [ ] Add accessory display on product pages
---
## Testing Scenarios
1. **Storm Door Selection**: Should skip material question (all Aluminum)
2. **Window Selection**: Should show material question (mixed materials)
3. **Single Color Products**: Should skip color question
4. **Multi-Color Products**: Should show color question
5. **Product Results**: Should show filtered products with compatible accessories
---
## Summary
**CSV**: No changes needed (unless adding bit values)
**JavaScript**: Major refactoring for conditional logic
**JSON**: New structure with filter objects and conditional flags
**Python**: Optional parsing script to generate JSON from CSV
+184
View File
@@ -0,0 +1,184 @@
# 🎉 Flask Web Application Successfully Created!
Your HTML/JavaScript quiz has been converted to a Python Flask web application.
## ✅ What Was Created
### Core Application Files
- **app.py** - Main Flask application with routes
- **wsgi.py** - Production WSGI entry point
- **config.py** - Configuration management
- **requirements.txt** - Python dependencies
### Templates & Static Files
- **templates/** - HTML files (index.html, index2.html, 404.html)
- **css/** - Stylesheets (styles.css)
- **js/** - JavaScript files (script.js)
- **images/** - Product images folder
### Documentation & Utilities
- **README.md** - Complete documentation
- **QUICKSTART.md** - Quick start guide
- **run.bat** - Windows startup script
- **.gitignore** - Git ignore file
- **.env.example** - Environment template
## 🚀 Quick Start
### Option 1: Using Batch File (Windows)
Double-click `run.bat` to install dependencies and start the server.
### Option 2: Manual Start
```bash
# Install dependencies
pip install -r requirements.txt
# Run the application
python app.py
```
### Access Your Application
Open in browser: **http://localhost:8080/quiz**
## 🌐 Current Status
✅ Flask is currently running on: http://localhost:8080
✅ Quiz page: http://localhost:8080/quiz
✅ Debug mode: Enabled (auto-reloads on file changes)
## 📁 Project Structure
```
project/
├── app.py ← Main Flask app (START HERE)
├── wsgi.py ← For production deployment
├── config.py ← Settings & configuration
├── requirements.txt ← Python packages needed
├── run.bat ← Windows startup script
├── templates/ ← HTML files (Flask requires this folder)
│ ├── index.html ← Main landing page
│ ├── index2.html ← Quiz page
│ └── 404.html ← Error page
├── css/ ← Stylesheets
│ └── styles.css ← Main CSS file
├── js/ ← JavaScript files
│ └── script.js ← Quiz logic and data
└── images/ ← Product images (add your images here)
```
## 🎯 Key Features
**Dynamic Routing** - Flask handles all page requests
**Static File Serving** - CSS, JS, and images properly served
**Error Handling** - Custom 404 page
**API Endpoints** - Ready for future backend features
**Production Ready** - WSGI config included
**All Original Features** - Quiz, forms, conditional logic, memory storage
## 🔧 Customization
### Change Port
Edit `app.py`, line with `app.run()`:
```python
app.run(debug=True, host='0.0.0.0', port=YOUR_PORT)
```
### Add New Routes
In `app.py`:
```python
@app.route('/your-page')
def your_page():
return render_template('your-page.html')
```
### Update Quiz Questions
Edit `js/script.js` - modify the `questionData` object
### Change Styling
Edit `css/styles.css`
## 📦 Deployment Options
### 1. Web Panels (cPanel, Plesk)
- Upload all files
- Install requirements: `pip install -r requirements.txt`
- Point to `wsgi.py`
### 2. Cloud Platforms
- **Heroku**: Add `Procfile` and push to Git
- **PythonAnywhere**: Upload and configure WSGI
- **AWS/Azure**: Use with Gunicorn
### 3. Docker
See README.md for Dockerfile example
### 4. Production Server
```bash
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app
```
## 🐛 Troubleshooting
### Port Already in Use
Change port in app.py or kill the process:
```bash
# Windows
netstat -ano | findstr :8080
taskkill /PID <PID> /F
```
### Templates Not Found
Make sure HTML files are in `templates/` folder
### Static Files Not Loading
Check that `css/` and `js/` folders exist in root directory
### Module Not Found
```bash
pip install -r requirements.txt
```
## 📚 Next Steps
1. **Test the application** - Visit http://localhost:8080/quiz
2. **Add your product images** - Place images in `images/` folder
3. **Customize the quiz** - Edit `js/script.js`
4. **Update styling** - Modify `css/styles.css`
5. **Deploy** - Follow README.md deployment guide
## 🔒 Security Notes for Production
Before deploying to production:
- [ ] Set `DEBUG = False` in config.py
- [ ] Change `SECRET_KEY` to a strong random value
- [ ] Use environment variables for sensitive data
- [ ] Set up HTTPS/SSL
- [ ] Use a production WSGI server (Gunicorn, uWSGI)
- [ ] Configure proper logging
- [ ] Set up database backups (if using a database)
## 💡 Tips
- Flask auto-reloads when you edit files (in debug mode)
- Press `Ctrl+C` to stop the server
- Check terminal for error messages
- Use browser DevTools to debug JavaScript
- All original quiz functionality is preserved
## 📞 Need Help?
- Check **README.md** for detailed documentation
- Check **QUICKSTART.md** for simple instructions
- Review Flask logs in terminal for errors
- Test API endpoints using browser or Postman
---
**Your Flask app is ready to use! 🎊**
Visit: http://localhost:8080/quiz
+311
View File
@@ -0,0 +1,311 @@
# Subdirectory Deployment Guide
This guide explains how to deploy the CGW Product Finder to a subdirectory on your web server (e.g., `http://example.com/cgwproducts/`).
## 🎯 Overview
The application is now configured to support subdirectory deployments through the `APPLICATION_ROOT` configuration variable. All templates use relative paths and `url_for()` to ensure proper routing regardless of deployment location.
## 🛠️ Configuration
### Method 1: Environment Variable (Recommended for Production)
Set the `APPLICATION_ROOT` environment variable before starting the application:
**Linux/Mac (Apache with Passenger):**
```bash
export APPLICATION_ROOT="/cgwproducts"
```
**Windows (IIS):**
Add to web.config or set in IIS environment variables:
```
APPLICATION_ROOT=/cgwproducts
```
**Apache .htaccess or Virtual Host:**
```apache
SetEnv APPLICATION_ROOT /cgwproducts
```
### Method 2: Modify config.py
Edit `app/config.py` and change the APPLICATION_ROOT line:
```python
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-here'
# Change this line to your subdirectory path
APPLICATION_ROOT = '/cgwproducts' # Or whatever your path is
```
**Important:** The path should:
- Start with `/`
- NOT end with `/`
- Match your web server configuration
### Method 3: Server Configuration
#### Apache with Passenger
If deploying to `http://example.com/cgwproducts/`:
```apache
<VirtualHost *:80>
ServerName example.com
# Document root is one level up from app folder
DocumentRoot /path/to/CGW Product Finder
# Set subdirectory as alias to app folder
Alias /cgwproducts /path/to/CGW Product Finder/app
<Directory "/path/to/CGW Product Finder/app">
Allow from all
Options -MultiViews
# Set APPLICATION_ROOT environment variable
SetEnv APPLICATION_ROOT /cgwproducts
# Enable Passenger
PassengerEnabled on
PassengerAppRoot /path/to/CGW Product Finder/app
PassengerPython /path/to/python3
</Directory>
# Block access to admin and data folders
<Directory "/path/to/CGW Product Finder/app/admin">
Require all denied
</Directory>
<Directory "/path/to/CGW Product Finder/app/data">
# Allow access only through Flask API
<FilesMatch "\\.json$">
Require all denied
</FilesMatch>
</Directory>
</VirtualHost>
```
#### Nginx with uWSGI
```nginx
server {
listen 80;
server_name example.com;
location /cgwproducts {
# Strip the /cgwproducts prefix when passing to Flask
rewrite ^/cgwproducts(.*)$ $1 break;
include uwsgi_params;
uwsgi_pass unix:/tmp/cgw-product-finder.sock;
# Set APPLICATION_ROOT
uwsgi_param APPLICATION_ROOT /cgwproducts;
}
# Block admin folder
location /cgwproducts/admin {
deny all;
}
}
```
## 📝 Python 3.13.11 Considerations
For **Python 3.13.11**, some packages may not have pre-built wheels yet.
### Updated requirements.txt
The requirements.txt has been updated to make Pillow optional:
```txt
Flask>=3.0.0
Werkzeug>=3.0.0
# Pillow>=10.0.0 # Optional - only for image generation
```
### Installation Steps
```bash
# Upgrade pip first
python -m pip install --upgrade pip
# Install core requirements (will work without Pillow)
pip install Flask>=3.0.0 Werkzeug>=3.0.0
# Try to install Pillow (optional)
pip install Pillow
# If Pillow fails, the app will still work but disable image generation
```
**Note:** Pillow build errors on Python 3.13.11 are common. If you don't need dynamic image generation, you can skip it.
## ✅ Testing Your Deployment
### Step 1: Verify Configuration
Check that APPLICATION_ROOT is set correctly:
```python
# Run in Python console
import os
print(os.environ.get('APPLICATION_ROOT', '/'))
```
### Step 2: Test Routes
If deployed to `/cgwproducts/`, test these URLs:
-`http://example.com/cgwproducts/` → Should redirect to login
-`http://example.com/cgwproducts/login` → Should show login page
-`http://example.com/cgwproducts/api/session` → Should return JSON
-`http://example.com/cgwproducts/css/styles.css` → Should load CSS
-`http://example.com/cgwproducts/js/script.js` → Should load JS
-`http://example.com/cgwproducts/data/products.json` → Should load data
### Step 3: Test Login Flow
1. Go to `/cgwproducts/login`
2. Login with Master / Master
3. Should redirect to `/cgwproducts/select-location`
4. Select a location
5. Should redirect to `/cgwproducts/`
6. User info should display in header
7. Click logout
8. Should return to `/cgwproducts/login`
### Step 4: Test User Management
1. Login as Master
2. On location selection page, click "User Management"
3. Should go to `/cgwproducts/users`
4. All buttons should work (Add User, Change Password, etc.)
## 🔧 Troubleshooting
### Issue: 404 on CSS/JS files
**Cause:** Static files not being served correctly
**Fix:** Ensure Flask routes for /css/, /js/, /data/ are working:
```bash
# Test directly
curl http://example.com/cgwproducts/css/styles.css
curl http://example.com/cgwproducts/js/script.js
```
### Issue: Login redirects to wrong path
**Cause:** APPLICATION_ROOT not set or incorrect
**Fix:**
1. Check environment variable: `echo $APPLICATION_ROOT`
2. Verify it matches your URL path
3. Restart web server after changing
### Issue: API calls return 404
**Cause:** API routes need APPLICATION_ROOT prefix
**Fix:** All templates now use `BASE_URL` variable:
```javascript
const BASE_URL = '{{ base_url }}'; // Automatically set by Flask
fetch(BASE_URL + '/api/login', {...})
```
### Issue: Cannot access /users or other protected pages
**Cause:** Session not persisting across requests
**Fix:**
1. Verify SECRET_KEY is set and doesn't change between restarts
2. Check cookie settings (SESSION_COOKIE_PATH should match APPLICATION_ROOT)
3. Ensure browser accepts cookies from subdirectory
### Issue: Module import errors after pip install
**Cause:** Pillow build failed on Python 3.13.11
**Fix:** Pillow is now optional. Application will work without it:
```bash
# Install without Pillow
pip install Flask>=3.0.0 Werkzeug>=3.0.0
# App will show: "Image generation not available"
# But all other features work
```
## 📂 Files Modified for Subdirectory Support
The following files have been updated to support subdirectory deployments:
### Backend:
- `app/config.py` - Added APPLICATION_ROOT configuration
- `app/app.py` - Added context processor for base_url
- `app/requirements.txt` - Made Pillow optional
### Templates (use {{ base_url }} and {{ url_for() }}):
- `app/templates/login.html`
- `app/templates/select_location.html`
- `app/templates/user_manager.html`
- `app/templates/index2.html`
- `app/templates/access_denied.html`
### JavaScript Updates:
All templates now define `BASE_URL` at the top of their scripts:
```javascript
const BASE_URL = '{{ base_url }}';
```
All fetch calls use: `fetch(BASE_URL + '/api/endpoint', ...)`
## 🚀 Deployment Checklist
Before deploying to a subdirectory:
- [ ] Set APPLICATION_ROOT environment variable or update config.py
- [ ] Install Flask and Werkzeug: `pip install Flask>=3.0.0 Werkzeug>=3.0.0`
- [ ] (Optional) Install Pillow: `pip install Pillow`
- [ ] Upload all modified files to server
- [ ] Configure web server (Apache/Nginx) with subdirectory path
- [ ] Set secure SECRET_KEY in production
- [ ] Block public access to /admin/ and /data/ folders
- [ ] Test all routes with subdirectory prefix
- [ ] Test login flow and session persistence
- [ ] Verify CSS/JS/images load correctly
- [ ] Test API endpoints return correct responses
## 📞 Need Help?
Common deployment paths:
- Root: `/` (default, no configuration needed)
- Application subdirectory: `/cgwproducts`
- User subdirectory: `/~username/cgwproducts`
- Domain subdirectory: `/app`
Whatever path you choose, set it as APPLICATION_ROOT and ensure your web server passes requests to Flask with that prefix.
## 🔐 Security Notes
When deploying to a subdirectory:
1. **SECRET_KEY** - Must be set and persistent across restarts
2. **Session Cookies** - Will be scoped to the subdirectory path
3. **Admin Folder** - Must be blocked from web access
4. **Data Folder** - JSON files should only be accessible through API
5. **HTTPS** - Use SSL/TLS in production and set SESSION_COOKIE_SECURE = True
## ✨ Benefits of This Approach
- ✅ Deploy to any path without code changes
- ✅ Works at root `/` or subdirectory `/cgwproducts/`
- ✅ All routes automatically adjust to deployment path
- ✅ No hardcoded URLs in templates or JavaScript
- ✅ Compatible with Apache, Nginx, IIS
- ✅ Passenger and uWSGI compatible
- ✅ Works with Python 3.13.11 (Pillow optional)
+294
View File
@@ -0,0 +1,294 @@
# Troubleshooting Guide - 404 Errors & Installation Issues
## 🔴 Issue 1: pip install requirements.txt fails
### Error Message:
```
error: subprocess-exited-with-error
× Getting requirements to build wheel did not run successfully.
```
### Solution:
This error usually happens with **Pillow** on Windows systems that don't have build tools installed.
#### Option 1: Install Pre-built Pillow (Recommended)
```bash
# Upgrade pip first
python -m pip install --upgrade pip
# Install Pillow from pre-built wheels
pip install --upgrade Pillow
# Then install other requirements
pip install Flask>=3.0.0
pip install Werkzeug>=3.0.0
```
#### Option 2: Install from updated requirements.txt
The requirements.txt has been updated to use version ranges instead of exact versions:
```bash
pip install -r requirements.txt
```
#### Option 3: Skip Pillow (if you don't need image generation)
If you don't need the dynamic image generation feature, you can install without Pillow:
```bash
pip install Flask>=3.0.0
pip install Werkzeug>=3.0.0
```
The app will still work - it will just disable the image generation features.
---
## 🔴 Issue 2: Login Page Returns 404 Error
### Common Causes & Solutions:
### Cause 1: Flask Server Not Running
**Check if the server is running:**
```bash
# Navigate to app folder
cd "C:\Users\Work\Desktop\CGW Product Finder\app"
# Run the Flask app
python app.py
```
You should see:
```
✓ Image generation available
* Running on http://127.0.0.1:8080
```
**Then access:** http://localhost:8080/login
---
### Cause 2: Wrong URL or Port
**Common mistakes:**
`http://localhost/login` - Missing port number
`http://localhost:8080/login` - Correct
`http://localhost:5000/login` - Wrong port
`http://localhost:8080/login` - Correct (app uses port 8080)
`/login` in browser address bar
`http://localhost:8080/login` - Need full URL
---
### Cause 3: Flask App Import Error
**Test if routes are registered:**
```bash
cd app
python test_routes.py
```
This will show:
- If Flask imports successfully
- All registered routes including /login
- What went wrong if there's an error
**Expected output:**
```
✓ Flask app imported successfully
📝 Login & Authentication Routes:
/login [GET] -> login_page
/api/login [POST] -> login
/api/logout [POST] -> logout
```
---
### Cause 4: Web Server Configuration (Apache/Passenger/IIS)
If you're running through a web server instead of `python app.py`:
**Check your server configuration:**
For **Apache + Passenger:**
- Verify passenger_wsgi.py is being used
- Check if Python path is correct in config
- Ensure the app folder is set as DocumentRoot
For **IIS:**
- Verify web.config is correct
- Check if Python handler is configured
- Ensure proper app folder path
**Test directly first:**
Always test with `python app.py` first to verify the app works before troubleshooting web server issues.
---
## 📋 Step-by-Step Troubleshooting
### Step 1: Verify Installation
```bash
# Check Python version (need 3.7+)
python --version
# Check if Flask is installed
python -c "import flask; print(flask.__version__)"
# Check if Werkzeug is installed
python -c "import werkzeug; print(werkzeug.__version__)"
```
### Step 2: Fresh Install
```bash
cd "C:\Users\Work\Desktop\CGW Product Finder\app"
# Upgrade pip
python -m pip install --upgrade pip
# Install requirements
pip install -r requirements.txt
# Or manual install
pip install Flask>=3.0.0 Werkzeug>=3.0.0 Pillow>=10.0.0
```
### Step 3: Test Routes
```bash
cd app
python test_routes.py
```
Expected: List of routes including /login
### Step 4: Start Server
```bash
python app.py
```
Expected output:
```
✓ Image generation available
* Running on http://127.0.0.1:8080
* Running on http://192.168.x.x:8080
```
### Step 5: Access in Browser
Open browser: http://localhost:8080/login
Expected: Login page with username/password fields
---
## 🐛 Common Errors & Fixes
### Error: "ModuleNotFoundError: No module named 'flask'"
```bash
pip install Flask>=3.0.0
```
### Error: "ModuleNotFoundError: No module named 'werkzeug'"
```bash
pip install Werkzeug>=3.0.0
```
### Error: "Address already in use" / "Port 8080 is already in use"
```bash
# Find what's using port 8080
netstat -ano | findstr :8080
# Kill the process (replace PID with actual process ID)
taskkill /PID <PID> /F
# Or edit app.py to use different port (line 911):
app.run(debug=True, host='0.0.0.0', port=8090)
```
### Error: "Template Not Found: login.html"
```bash
# Verify template exists
dir templates\login.html
# If missing, the file needs to be uploaded to the server
```
### Error: "Working outside of application context"
This means Flask app isn't initialized properly. Run `python test_routes.py` to diagnose.
---
## ✅ Verification Checklist
After fixing issues, verify:
- [ ] Flask server starts without errors: `python app.py`
- [ ] http://localhost:8080/ redirects to http://localhost:8080/login ✓
- [ ] http://localhost:8080/login shows login page ✓
- [ ] Can login with username: `Master` password: `Master`
- [ ] After login, see location selection page ✓
- [ ] Can access user management (Master user only) ✓
- [ ] Logout works and returns to login page ✓
---
## 🚀 Production Deployment
For production servers (not localhost):
1. **Use proper WSGI server** (not `python app.py`)
- Passenger (Apache/Nginx)
- uWSGI
- Gunicorn (Linux)
2. **Set secure SECRET_KEY** in config.py
```python
# Don't use secrets.token_hex() in production
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secure-random-key-here'
```
3. **Enable HTTPS** and update config:
```python
SESSION_COOKIE_SECURE = True # Only send cookies over HTTPS
```
4. **Set Debug=False** for production
---
## 📞 Still Having Issues?
1. Run the test script:
```bash
python test_routes.py
```
2. Check Flask server output for errors:
```bash
python app.py
```
Look for red error messages
3. Test with curl:
```bash
curl http://localhost:8080/login
```
Should return HTML, not 404
4. Check browser console for JavaScript errors (F12)
5. Verify file permissions (server can read templates/)
---
## 📁 Files Needed for Login System
Ensure these files exist and are uploaded:
- ✅ app/app.py
- ✅ app/templates/login.html
- ✅ app/templates/user_manager.html
- ✅ app/templates/access_denied.html
- ✅ app/templates/select_location.html
- ✅ app/templates/index2.html
- ✅ app/css/styles.css
- ✅ app/data/users.json
Missing any of these will cause 404 or errors.
+352
View File
@@ -0,0 +1,352 @@
# URL State Example - Navigation Flow
## 📍 Visual Journey Through URL Changes
### Step 1: Page Load
```
URL: https://yoursite.com/
State: Fresh start, no parameters
Bit Value: 0
```
```
┌─────────────────────────────────┐
│ What are you looking for? │
│ ┌─────┐ ┌─────┐ │
│ │ 🚪 │ │ 🪟 │ │
│ │Door │ │Window│ │
│ └─────┘ └─────┘ │
└─────────────────────────────────┘
```
---
### Step 2: User Selects "Door"
```
URL: https://yoursite.com/?b=1&q=q-door-type
Bit 0 set (Door)
State: Door selected
Bit Value: 1 (binary: 1)
Bits Active: Door
```
```
┌─────────────────────────────────┐
│ What type of door? │
│ ┌──────────┐ ┌──────────┐ │
│ │Storm Door│ │Patio Door│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────┘
```
---
### Step 3: User Selects "Storm Door"
```
URL: https://yoursite.com/?b=16385&q=q-material-storm-door
16385 = 1 (Door) + 16384 (Storm Door)
State: Door + Storm Door selected
Bit Value: 16385 (binary: 100000000000001)
Bits Active: Door, Storm Door subtype
Calculation:
Bit 0 (Door) = 1
Bit 14 (Storm Door)= 16384
Total = 16385
```
```
┌─────────────────────────────────┐
│ Select Material │
│ CONDITIONAL: Only showing │
│ because some storm doors have │
│ multiple materials │
│ ┌─────────┐ ┌─────────┐ │
│ │Aluminum │ │ Vinyl │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────┘
```
---
### Step 4: User Selects "Aluminum"
```
URL: https://yoursite.com/?b=16401&q=q-color-storm-door
16401 = 1 + 16 + 16384
State: Door + Aluminum + Storm Door
Bit Value: 16401 (binary: 100000000010001)
Bits Active: Door, Aluminum, Storm Door subtype
Calculation:
Bit 0 (Door) = 1
Bit 4 (Aluminum) = 16
Bit 14 (Storm Door)= 16384
Total = 16401
```
```
┌─────────────────────────────────┐
│ Select Color │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ Black │ │ White │ │Bronze │ │
│ └───────┘ └───────┘ └───────┘ │
└─────────────────────────────────┘
```
---
### Step 5: User Selects "White"
```
URL: https://yoursite.com/?b=16529&q=q-dimensions
16529 = 1 + 16 + 128 + 16384
State: Door + Aluminum + White + Storm Door
Bit Value: 16529 (binary: 100000010010001)
Bits Active: Door, Aluminum, White, Storm Door
Calculation:
Bit 0 (Door) = 1
Bit 4 (Aluminum) = 16
Bit 7 (White) = 128
Bit 14 (Storm Door)= 16384
Total = 16529
```
```
┌─────────────────────────────────┐
│ Enter Dimensions │
│ Width: [______] inches │
│ Height: [______] inches │
│ [Continue →] │
└─────────────────────────────────┘
```
---
### Step 6: View Results
```
URL: https://yoursite.com/?b=16529&q=results
State: Showing filtered products
Bit Value: 16529
Products Matched: All doors that are:
✓ Storm Door type (bit 14)
✓ Aluminum material (bit 4)
✓ Available in White (bit 7)
```
```
┌─────────────────────────────────┐
│ Found 3 Products │
│ ┌─────────────────────────┐ │
│ │ STAR 6100 FULL VIEW │ │
│ │ Code: 6100I │ │
│ │ Aluminum, White, Bronze │ │
│ │ [View Details] │ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ COLUMBIA COBRA │ │
│ │ Code: COBRAI │ │
│ │ [View Details] │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
```
---
### Step 7: Click Product Detail
```
URL: https://yoursite.com/?p=6100I&b=16529
Product code added
State: Viewing specific product 6100I
Bit Value: 16529 (preserved for "back" navigation)
```
```
┌─────────────────────────────────────────┐
│ STAR 6100 FULL VIEW STORM DOOR │
│ ────────────────────────────────── │
│ Product Code: 6100I │
│ Category: STD │
│ Materials: Aluminum │
│ Colors: White, Bronze │
│ │
│ Compatible Accessories: │
│ • BGIST - Inserts for Storm Doors │
│ • TGIODD - Top Glass Inserts │
│ │
│ [← Back] [Start Over] [🔗 Share] │
└─────────────────────────────────────────┘
```
---
### Step 8: Copy & Share URL
```
User clicks "🔗 Share" button
Copies: https://yoursite.com/?p=6100I&b=16529
Someone else pastes this URL in their browser:
→ Instantly shows product 6100I
→ Navigation state preserved (b=16529)
→ Back button navigates through original path
```
---
### Step 9: Browser Back Button
```
User clicks browser back button
URL Changes:
https://yoursite.com/?p=6100I&b=16529
https://yoursite.com/?b=16529&q=results
https://yoursite.com/?b=16529&q=q-dimensions
https://yoursite.com/?b=16401&q=q-color-storm-door
https://yoursite.com/?b=16385&q=q-material-storm-door
https://yoursite.com/?b=1&q=q-door-type
https://yoursite.com/
Each step backward:
✓ Restores question state
✓ Maintains bit value
✓ Shows correct UI
```
---
### Step 10: Refresh Page
```
At any point, user presses F5 to refresh
Before Refresh: https://yoursite.com/?b=16529&q=q-dimensions
After Refresh: https://yoursite.com/?b=16529&q=q-dimensions
Result:
✓ Returns to same question
✓ Previous answers restored
✓ Accumulated selections preserved
✓ Can continue where they left off
```
---
## 🎯 Bit Value Breakdown Reference
### Common Values You'll See
| Bit Value | Decimal | What It Means |
|-----------|---------|---------------|
| `0000...0001` | 1 | Door selected |
| `0000...0010` | 2 | Window selected |
| `0001...0001` | 16385 | Door + Storm Door |
| `0001...0011` | 16387 | Door + Window + Storm Door |
| `0001...0011` | 16401 | Door + Aluminum + Storm Door |
| `0010...0010` | 16529 | Door + Aluminum + White + Storm Door |
### Quick Decode Formula
```
To check if a bit is set:
if (bitValue & (1 << bitPosition)) {
// That attribute is selected
}
Examples:
16529 & 1 = 1 → Door is selected ✓
16529 & 2 = 0 → Window is NOT selected
16529 & 16 = 16 → Aluminum is selected ✓
16529 & 128 = 128 → White is selected ✓
16529 & 16384 = 16384 → Storm Door is selected ✓
```
---
## 🔗 URL Patterns
### Navigation State
```
Pattern: /?b={bitValue}&q={questionKey}
Example: /?b=16529&q=q-dimensions
Use: Bookmark navigation progress
```
### Product View
```
Pattern: /?p={productCode}&b={bitValue}
Example: /?p=6100I&b=16529
Use: Direct link to product with context
```
### Initial State
```
Pattern: /
Example: https://yoursite.com/
Use: Fresh start, no parameters
```
---
## 💡 Power User Features
### Hack the URL
Users can manually modify URLs:
```
Original: /?b=16529&q=q-dimensions
(Door + Aluminum + White + Storm Door)
Modified: /?b=34&q=q-dimensions
(Vinyl only)
Result: Jumps directly to Vinyl products
```
### Preset Configurations
Your sales team can create bookmarks:
```
Residential Storm Doors:
/?b=16401&q=results
Commercial Windows:
/?b=8194&q=results
Budget Options (Vinyl):
/?b=32&q=start
```
### Analytics Tracking
Track which combinations are popular:
```
Top URLs:
/?b=16529 → White Aluminum Storm Doors (2,453 views)
/?b=8226 → White Vinyl Windows (1,834 views)
/?b=272 → Bronze products (892 views)
```
---
## 🎓 Learning Example
### Try This Exercise:
1. Start at homepage (b=0)
2. Select Window (b=2)
3. Select Primary Window (b=8194)
4. Select Vinyl (b=8226)
5. Select White (b=8354)
Your URL should be: `/?b=8354&q=q-dimensions`
Decode: 8354 in binary = 10000010100010
- Bit 1 (2) = Window ✓
- Bit 5 (32) = Vinyl ✓
- Bit 7 (128) = White ✓
- Bit 13 (8192) = Primary Window ✓
Total: 2 + 32 + 128 + 8192 = 8354 ✓
---
Ready to implement! 🚀
+230
View File
@@ -0,0 +1,230 @@
# Implementation Summary - URL State Management
## 🎯 What You Get
Your application will support **shareable URLs** that preserve:
- ✅ User's navigation progress (bitwise value)
- ✅ Current question position
- ✅ Direct product links
- ✅ Browser refresh without data loss
- ✅ Browser back/forward buttons
- ✅ Copy/share functionality
## 🔗 URL Examples
```
# Initial state (no params)
https://yoursite.com/
# After selecting Door + Storm Door + Aluminum
https://yoursite.com/?b=16401&q=q-dimensions
↑ ↑
| Current question
Accumulated bit value (Door + Aluminum + Storm Door)
# Viewing specific product
https://yoursite.com/?p=BGIST&b=16401
Product code
# Bit value 16401 decodes to:
# Bit 0 (1) = Door
# Bit 4 (16) = Aluminum
# Bit 14 (16384) = Storm Door subtype
# Total: 1 + 16 + 16384 = 16401
```
## 📋 Implementation Checklist
### Phase 1: Core URL Functionality (Essential)
- [ ] Add bitwise data loading to `init()` function
- [ ] Add `accumulatedBitValue` variable
- [ ] Add `BIT_DEFINITIONS` constants
- [ ] Implement `updateURL()` function
- [ ] Implement `updateBitValue()` function
- [ ] Update `handleAnswer()` to call `updateBitValue()`
- [ ] Implement `initFromURL()` function
- [ ] Implement `restoreStateFromBitValue()` function
- [ ] Update `startOver()` to clear URL
### Phase 2: Product Deep Linking (Recommended)
- [ ] Implement `showProductByCode()` function
- [ ] Update `showProductDetail()` to update URL
- [ ] Make product cards clickable
- [ ] Add URL params when viewing products
### Phase 3: Share Functionality (Nice to Have)
- [ ] Implement `shareCurrentPage()` function
- [ ] Add `showNotification()` helper
- [ ] Add "Share" buttons to UI
- [ ] Add CSS for notification animations
### Phase 4: Browser Navigation (Polish)
- [ ] Add `popstate` event listener
- [ ] Test browser back button
- [ ] Test browser forward button
- [ ] Test refresh behavior
## 🚀 Quick Start
### Step 1: Add Global Variables
Add to top of `js/script.js`:
```javascript
let accumulatedBitValue = 0;
let bitwiseData = {};
const BIT_DEFINITIONS = {
'base_door': 0, 'base_window': 1,
'material_aluminum': 4, 'material_vinyl': 5,
'color_black': 6, 'color_white': 7, 'color_bronze': 8,
'color_tan': 9, 'color_mill': 10, 'color_sandstone': 11,
'subtype_patio_door': 12, 'subtype_primary_window': 13,
'subtype_storm_door': 14, 'subtype_storm_window': 15
};
```
### Step 2: Load Bitwise Data
Update `init()` to load `product_bitwise.json`:
```javascript
Promise.all([
fetch('data/navigation.json').then(r => r.json()),
fetch('data/products.json').then(r => r.json()),
fetch('data/accessories.json').then(r => r.json()),
fetch('data/product_bitwise.json').then(r => r.json()) // ADD THIS
])
.then(([questions, products, accessories, bitwise]) => {
// ... existing code ...
// Index bitwise data
bitwiseData = {};
bitwise.forEach(item => {
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
});
// Check for URL state
initFromURL();
});
```
### Step 3: Add URL Functions
Copy these three key functions from [URL_STATE_MANAGEMENT.md](URL_STATE_MANAGEMENT.md):
1. `updateURL()` - Updates browser URL
2. `updateBitValue()` - Calculates bit value from selection
3. `initFromURL()` - Restores state from URL on load
### Step 4: Update handleAnswer
Add one line to `handleAnswer()`:
```javascript
function handleAnswer(currentKey, answerValue, answerIndex) {
// ... existing code ...
updateBitValue(answerObject); // ADD THIS LINE
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
history.push(nextKey);
updateURL(nextKey); // ADD THIS LINE
loadContent(nextKey);
}
```
### Step 5: Test
1. Run your app
2. Navigate through questions
3. Check URL updates after each selection
4. Copy URL and paste in new tab
5. Should restore to same state ✅
## 📖 Documentation Files
All details are in these files:
- **[URL_STATE_MANAGEMENT.md](URL_STATE_MANAGEMENT.md)** - Complete implementation guide
- **[REQUIRED_CODE_CHANGES.md](REQUIRED_CODE_CHANGES.md)** - Updated with URL features
- **[BITWISE_USAGE_GUIDE.md](BITWISE_USAGE_GUIDE.md)** - How bitwise system works
## 🔧 Key Functions Reference
| Function | Purpose | When Called |
|----------|---------|-------------|
| `initFromURL()` | Read URL params on page load | Once at startup |
| `updateURL()` | Write current state to URL | After each answer |
| `updateBitValue()` | Add selection to bit value | After each answer |
| `restoreStateFromBitValue()` | Decode bit value to selections | On page load from URL |
| `shareCurrentPage()` | Copy URL to clipboard | User clicks "Share" |
## 🎨 URL Format Design
### Why Bitwise?
- **Compact**: `?b=16401` vs `?door=true&aluminum=true&storm=true`
- **Fast**: Single integer comparison
- **Flexible**: Easy to add new attributes
- **Shareable**: Short URLs
- **Reversible**: Can decode back to selections
### Parameters Chosen
- `b` = bit value (short, recognizable)
- `q` = question (short, clear purpose)
- `p` = product (short, clear purpose)
### Alternative Considered
Could use hash fragments instead:
```
https://yoursite.com/#/door/storm-door/aluminum
```
But query params are better for:
- Server-side rendering
- Analytics tracking
- SEO (if products are indexed)
## ⚠️ Important Notes
1. **Bit Definitions Must Match**: The `BIT_DEFINITIONS` in JavaScript must match the Python script
2. **URL Length Limits**: URLs have practical limits (~2000 chars), but bit values are small
3. **No Sensitive Data**: Don't put sensitive info in URL (it's visible and loggable)
4. **Test Thoroughly**: Test all navigation paths and browser actions
## 🐛 Troubleshooting
### URL Not Updating
- Check `updateURL()` is called after `handleAnswer()`
- Check browser console for errors
- Verify `accumulatedBitValue` is being set
### State Not Restoring
- Check `initFromURL()` is called in `init()`
- Verify URL has `b` and `q` parameters
- Check `restoreStateFromBitValue()` logic
### Wrong Bit Values
- Verify `BIT_DEFINITIONS` matches `generate_bitwise_helper.py`
- Check `bitwise_legend.json` for correct bit positions
- Use browser console: `console.log(accumulatedBitValue)`
### Share Button Not Working
- Check clipboard API support: `navigator.clipboard`
- Fallback to `prompt()` for older browsers
- Test in HTTPS (clipboard API requires secure context)
## 📈 Benefits Summary
| Feature | User Benefit | Business Benefit |
|---------|-------------|------------------|
| Shareable URLs | Share configurations | Viral marketing |
| Bookmarks | Save favorites | Return visitors |
| Refresh-safe | No data loss | Better UX |
| Deep linking | Direct to product | SEO indexing |
| Browser nav | Back/forward works | Expected behavior |
| Short URLs | Easy to share | More sharing |
## 🎯 Next Steps
1. ✅ Implement Phase 1 (core URL functionality)
2. ✅ Test basic URL state restoration
3. ✅ Add Phase 2 (product deep linking)
4. ✅ Add Phase 3 (share buttons)
5. ✅ Add Phase 4 (browser navigation)
6. ✅ Test all scenarios thoroughly
7. ✅ Add analytics tracking (optional)
Good luck! 🚀
+514
View File
@@ -0,0 +1,514 @@
# URL State Management Implementation
## Overview
This feature allows users to:
1. **Bookmark** their progress through the navigation flow
2. **Refresh** the page without losing their selections
3. **Share** URLs with specific products or navigation states
4. **Deep link** directly to products
## URL Parameter Structure
### Query Parameters
- `b` - Bitwise value representing all selections (integer)
- `q` - Current question ID (string)
- `p` - Product code for direct product view (string)
### Examples
```
# At start
https://yoursite.com/
# After selecting Door > Storm Door > Aluminum
https://yoursite.com/?b=16401&q=q-dimensions
# Viewing a specific product
https://yoursite.com/?p=BGIST&b=16401
# Bit value 16401 decodes to:
# - Door (bit 0)
# - Aluminum (bit 4)
# - Storm Door subtype (bit 14)
```
## Implementation Code
### 1. Add to Global Variables (top of script.js)
```javascript
// Add these to existing global variables
let accumulatedBitValue = 0;
let bitwiseData = {};
// Bit definitions (must match generate_bitwise_helper.py)
const BIT_DEFINITIONS = {
// Base Types
'base_door': 0, // 1
'base_window': 1, // 2
// Flags
'is_accessory': 2, // 4
'specific_item': 3, // 8
// Materials
'material_aluminum': 4, // 16
'material_vinyl': 5, // 32
// Colors
'color_black': 6, // 64
'color_white': 7, // 128
'color_bronze': 8, // 256
'color_tan': 9, // 512
'color_mill': 10, // 1024
'color_sandstone': 11, // 2048
// Subtypes (from bitwise_legend.json)
'subtype_patio_door': 12, // 4096
'subtype_primary_window': 13, // 8192
'subtype_storm_door': 14, // 16384
'subtype_storm_window': 15 // 32768
};
```
### 2. Update init() Function
```javascript
function init() {
// Load all data files (including bitwise)
Promise.all([
fetch('data/navigation.json').then(r => r.json()),
fetch('data/products.json').then(r => r.json()),
fetch('data/accessories.json').then(r => r.json()),
fetch('data/product_bitwise.json').then(r => r.json())
])
.then(([questions, products, accessories, bitwise]) => {
questionData = questions;
productData = products;
accessoryData = accessories;
// Index bitwise data by product code
bitwiseData = {};
bitwise.forEach(item => {
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
});
// Initialize from URL or start fresh
initFromURL();
})
.catch(error => {
console.error('Error loading data:', error);
showError('Failed to load application data.');
});
}
```
### 3. URL Initialization
```javascript
function initFromURL() {
const params = new URLSearchParams(window.location.search);
// Priority 1: Direct product view
const productCode = params.get('p');
if (productCode) {
accumulatedBitValue = parseInt(params.get('b') || '0', 10);
showProductByCode(productCode);
return;
}
// Priority 2: Restore navigation state
const bitValue = params.get('b');
const questionKey = params.get('q');
if (bitValue && questionKey) {
accumulatedBitValue = parseInt(bitValue, 10);
restoreStateFromBitValue(accumulatedBitValue, questionKey);
return;
}
// Priority 3: Start fresh
loadContent('start');
}
function restoreStateFromBitValue(bitValue, questionKey) {
// Decode bit value back to user selections
// Base type
if (bitValue & 1) {
userAnswers['start'] = 'Door';
} else if (bitValue & 2) {
userAnswers['start'] = 'Window';
}
// Materials
if (bitValue & 16) {
userAnswers['q-material.material'] = 'Aluminum';
} else if (bitValue & 32) {
userAnswers['q-material.material'] = 'Vinyl';
}
// Colors
const colorMap = {
64: 'Black',
128: 'White',
256: 'Bronze',
512: 'Tan',
1024: 'Mill',
2048: 'Sandstone'
};
for (const [bit, color] of Object.entries(colorMap)) {
if (bitValue & parseInt(bit)) {
userAnswers['q-color.color'] = color;
break; // Only store first color found
}
}
// Subtypes
const subtypeMap = {
4096: 'Patio Door',
8192: 'Primary Window',
16384: 'Storm Door',
32768: 'Storm Window'
};
for (const [bit, subtype] of Object.entries(subtypeMap)) {
if (bitValue & parseInt(bit)) {
userAnswers['q-subtype'] = subtype;
break; // Only store first subtype found
}
}
// Navigate to the saved question
history = ['start'];
if (questionKey !== 'start') {
history.push(questionKey);
}
loadContent(questionKey);
}
```
### 4. Update URL on Each Selection
```javascript
function updateURL(questionKey = null) {
const params = new URLSearchParams();
if (accumulatedBitValue > 0) {
params.set('b', accumulatedBitValue.toString());
}
if (questionKey) {
params.set('q', questionKey);
} else if (history.length > 0) {
params.set('q', history[history.length - 1]);
}
const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
window.history.replaceState(
{
bitValue: accumulatedBitValue,
questionKey: questionKey
},
'',
newURL
);
}
function updateBitValue(answerObject) {
if (!answerObject.filter) return;
const filter = answerObject.filter;
// Base type
if (filter.baseType === 'Door') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_door']);
} else if (filter.baseType === 'Window') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_window']);
}
// Materials
if (filter.material === 'Aluminum') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_aluminum']);
} else if (filter.material === 'Vinyl') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_vinyl']);
}
// Colors
const colorKey = filter.color ? 'color_' + filter.color.toLowerCase() : null;
if (colorKey && BIT_DEFINITIONS[colorKey]) {
accumulatedBitValue |= (1 << BIT_DEFINITIONS[colorKey]);
}
// Subtypes
const subtypeKey = filter.subType ? 'subtype_' + filter.subType.toLowerCase().replace(/ /g, '_') : null;
if (subtypeKey && BIT_DEFINITIONS[subtypeKey]) {
accumulatedBitValue |= (1 << BIT_DEFINITIONS[subtypeKey]);
}
}
```
### 5. Update handleAnswer Function
```javascript
function handleAnswer(currentKey, answerValue, answerIndex) {
const currentQuestion = questionData[currentKey];
const answerObject = currentQuestion.answers[answerIndex];
// Store the answer
userAnswers[currentKey] = answerValue;
// Update bit value based on selection
updateBitValue(answerObject);
// Resolve the next question (handles conditionals)
const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject);
history.push(nextKey);
// Update URL with new state
updateURL(nextKey);
loadContent(nextKey);
}
```
### 6. Product View with URL
```javascript
function showProductByCode(productCode) {
const product = productData.find(p => p.productCode === productCode);
if (!product) {
showError(`Product ${productCode} not found`);
return;
}
showProductDetail(product);
}
function showProductDetail(product) {
// Update URL with product code
const params = new URLSearchParams();
params.set('p', product.productCode);
if (accumulatedBitValue > 0) {
params.set('b', accumulatedBitValue.toString());
}
window.history.pushState(
{ productCode: product.productCode },
'',
'?' + params.toString()
);
// Render product detail
const compatibleAccessories = accessoryData.filter(acc =>
product.compatibleAccessories && product.compatibleAccessories.includes(acc.id)
);
const contentDiv = document.getElementById('content');
let html = `
<div class="result-container">
<div class="result-title">${product.description}</div>
<div class="result-content">
<div class="result-details">
<p><strong>Product Code:</strong> ${product.productCode}</p>
<p><strong>Category:</strong> ${product.category}</p>
<p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
<p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
<p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
${compatibleAccessories.length > 0 ? `
<h3 style="margin-top: 20px;">Compatible Accessories</h3>
<div class="accessories-list">
${compatibleAccessories.map(acc => `
<div class="accessory-item">
<strong>${acc.description}</strong><br>
<small>Code: ${acc.accessoryCode} | ${acc.materials.join(', ')}</small>
</div>
`).join('')}
</div>
` : ''}
</div>
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Go Back</button>
<button class="back-button" onclick="startOver()">Start Over</button>
<button class="back-button" onclick="shareCurrentPage()">🔗 Share Product</button>
</div>
</div>
`;
contentDiv.innerHTML = html;
}
```
### 7. Share Functionality
```javascript
function shareCurrentPage() {
const url = window.location.href;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => {
showNotification('Link copied to clipboard!');
}).catch(() => {
showUrlPrompt(url);
});
} else {
showUrlPrompt(url);
}
}
function showUrlPrompt(url) {
const message = prompt('Copy this URL to share:', url);
}
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
```
### 8. Update startOver Function
```javascript
function startOver() {
history = ['start'];
// Clear all stored answers
for (const key in userAnswers) {
delete userAnswers[key];
}
// Clear bit value
accumulatedBitValue = 0;
// Clear URL (return to root)
window.history.replaceState({}, '', window.location.pathname);
loadContent('start');
}
```
### 9. Handle Browser Back Button
```javascript
// Add this to init() or as separate event listener
window.addEventListener('popstate', function(event) {
if (event.state) {
if (event.state.productCode) {
showProductByCode(event.state.productCode);
} else if (event.state.questionKey) {
accumulatedBitValue = event.state.bitValue || 0;
loadContent(event.state.questionKey);
} else {
startOver();
}
} else {
// No state, check URL
initFromURL();
}
});
```
### 10. Add CSS for Notification
```css
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.accessory-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 3px solid #2196F3;
border-radius: 3px;
}
.product-card {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
```
## Testing Scenarios
### Test 1: Basic Navigation
1. Start quiz
2. Select Door → Storm Door → Aluminum → White
3. Check URL contains `?b=XXXX&q=YYYY`
4. Copy URL
5. Open in new tab → Should restore to same point
### Test 2: Product Deep Link
1. Navigate to product BGIST
2. Check URL contains `?p=BGIST&b=XXXX`
3. Copy URL
4. Open in new tab → Should show product directly
### Test 3: Browser Refresh
1. Navigate through several questions
2. Press F5 to refresh
3. Should restore to same question with selections intact
### Test 4: Browser Back Button
1. Navigate forward through questions
2. Press browser back button
3. Should step backward through questions
4. URL should update accordingly
### Test 5: Share Button
1. Complete navigation flow
2. Click "Share" button
3. Should copy URL to clipboard
4. Paste in new browser → Should restore state
## Benefits
1. **User Experience**: Users don't lose progress on refresh
2. **Shareability**: Users can share specific configurations
3. **Bookmarking**: Useful configurations can be saved
4. **SEO**: Product pages are directly linkable
5. **Analytics**: Track specific navigation paths via URL parameters
6. **Support**: Users can share URLs when asking for help
## URL Encoding Notes
- Bit values are stored as decimal integers (more compact than hex for small values)
- Product codes are URL-safe (no special encoding needed)
- Question IDs are alphanumeric (q-dimensions, etc.)
- Special characters in product codes should be URL-encoded if present
+343
View File
@@ -0,0 +1,343 @@
# User Management System
## Overview
A secure user management front-end that allows you to create and manage users. Each user record contains:
- **Username**: Unique identifier
- **Password**: Securely hashed using PBKDF2-SHA256 (industry-standard)
- **Default Location**: Primary location (required) - one of: Lindsborg, Iola, KC, or BMD
- **Active Status**: Whether the user account is active or inactive (toggleable in user list)
- **Location Settings**: Per-location configuration with:
- **Accessible**: Whether the user can access this location
## User Interface Features
### Table-Based Location Configuration
The form uses an intuitive table layout with:
- **Default Location Column**: Radio buttons to select ONE primary location (required)
- **Accessible Column**: Checkboxes to mark which locations the user can access
### User List with Active Toggle
Each user in the list shows:
- **Username** and **Location Information**
- **Active/Inactive Toggle**: Click to enable or disable the user account
- Active users have full border color
- Inactive users are dimmed with reduced opacity
- **Delete Button**: Remove the user permanently
### Smart Header Checkboxes
The accessible column has a header checkbox that:
- Shows three states: checked ✓, unchecked ☐, or indeterminate ⊟ (mixed)
- **Clicking cycles**: If off or mixed → all on, if on → all off
- **Auto-updates**: When you check/uncheck individual rows, the header shows:
- ✓ if all are checked
- ☐ if none are checked
- ⊟ if some are checked (mixed state)
### Extensible Design
The table structure is designed to easily add more columns in the future:
- Permission columns (Permission 1, Permission 2, etc.)
- Custom attributes
- Feature flags
- Any other per-location settings
Each new column can have the same header checkbox behavior.
## Security Features
- **Password Hashing**: Passwords are hashed using `pbkdf2:sha256` algorithm
- **Not Plain Text**: Passwords are never stored in plain text
- **Cryptographically Secure**: Uses Werkzeug's secure password hashing
- **Cannot Be Decoded**: Hashed passwords cannot be reversed back to plain text
## How to Use
### 1. Access the User Manager
Navigate to: `http://localhost:8080/users`
### 2. Add New Users
- Fill in the form with username, password, and location
- Click "Add User" button
- User will be added with a securely hashed password
### 3. View Users
- All users are displayed in a list showing username and location
- Password hashes are NOT displayed for security
### 4. Download Users JSON
- Click "📥 Download Users JSON" button
- Downloads a `users.json` file containing all users
- Password field contains the secure hash (not plain text)
### 5. Delete Users
- Click "Delete" next to any user to remove them
- Click "🗑️ Clear All Users" to remove all users at once
## API Endpoints
### GET /users
Displays the user management interface.
### GET /api/users
Returns all users in JSON format.
**Response:**
```json
{
"status": "success",
"users": [
{
"username": "john_doe",
"password": "pbkdf2:sha256:600000$...",
"defaultLocation": "LINDS",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": false },
"BMD": { "accessible": false }
}
}
]
}
```
### POST /api/users
Adds a new user.
**Request Body:**
```json
{
"username": "jane_smith",
"password": "mySecurePassword123",
"defaultLocation": "KC",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": true },
"BMD": { "accessible": true }
}
}
```
**Response:**
```json
{
"status": "success",
"message": "User added successfully",
"users": [...]
}
```
### DELETE /api/users/{index}
Deletes a user by their index position.
### PATCH /api/users/{index}/active
Toggle user active status.
**Request Body:**
```json
{
"active": true
}
```
### DELETE /api/users/clear
Clears all users from the system.
### GET /api/users/download
Downloads all users as a JSON file.
## Password Security
### How Passwords Are Stored
Passwords are hashed using PBKDF2-SHA256 with the following properties:
- **Algorithm**: PBKDF2 (Password-Based Key Derivation Function 2)
- **Hash Function**: SHA-256
- **Iterations**: 600,000+ (computationally expensive for attackers)
- **Salt**: Automatically generated unique salt per password
### Example Hash Format
```
pbkdf2:sha256:600000$AbCdEfGh$1234567890abcdef...
```
Components:
- `pbkdf2:sha256` - Algorithm identifier
- `600000` - Number of iterations
- `$AbCdEfGh` - Random salt
- `$1234567890abcdef...` - Actual hash
### Password Verification
To verify a password, use Werkzeug's `check_password_hash()`:
```python
from werkzeug.security import check_password_hash
# user['password'] contains the hash
if check_password_hash(user['password'], provided_password):
print("Password is correct!")
```
## Data Storage
Users are stored in: `app/data/users.json`
**Example users.json:**
```json
[
{
"username": "admin",
"password": "pbkdf2:sha256:600000$r7K8L9M0$a1b2c3d4e5f6...",
"defaultLocation": "LINDS",
"active": true,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": false },
"BMD": { "accessible": false }
}
},
{
"username": "user1",
"password": "pbkdf2:sha256:600000$n5O6P7Q8$x9y8z7w6v5u4...",
"defaultLocation": "KC",
"active": false,
"locationSettings": {
"LINDS": { "accessible": true },
"IOLA": { "accessible": true },
"KC": { "accessible": true },
"BMD": { "accessible": true }
}
}
]
```
### Location Codes
- **LINDS** = Lindsborg
- **IOLA** = Iola
- **KC** = KC
- **BMD** = BMD
## Integration Example
### Authenticate and Get User Info
```python
from werkzeug.security import check_password_hash
import json
def authenticate_user(username, password):
"""Authenticate a user by username and password"""
with open('app/data/users.json', 'r') as f:
users = json.load(f)
# Find user
user = next((u for u in users if u['username'] == username), None)
if user and check_password_hash(user['password'], password):
return True, user
return False, None
# Usage
success, user_data = authenticate_user('john_doe', 'password123')
if success:
print(f"Welcome {user_data['username']}!")
print(f"Default location: {user_data['defaultLocation']}")
print(f"Account active: {user_data.get('active', True)}")
# Check if user can access a location
if user_data['locationSettings']['KC']['accessible']:
print("User can access KC")
```
### Check User Permissions for a Location
```python
def can_access_location(user_data, location_code):
"""Check if user can access a specific location"""
return user_data.get('locationSettings', {}).get(location_code, {}).get('accessible', False)
def is_user_active(user_data):
"""Check if user account is active"""
return user_data.get('active', True)
# Usage
if not is_user_active(user_data):
print("User account is inactive")
return
if can_access_location(user_data, 'LINDS'):
print("User can access Lindsborg")
```
### Get All Accessible Locations for a User
```python
def get_accessible_locations(user_data):
"""Get all locations where user has access"""
accessible_locations = []
for location_code, settings in user_data.get('locationSettings', {}).items():
if settings.get('accessible', False):
accessible_locations.append(location_code)
return accessible_locations
# Usage
accessible = get_accessible_locations(user_data)
print(f"User can access: {', '.join(accessible)}")
```
## Notes
- Username must be unique
- Minimum password length: 6 characters
- Default location is required (one of: Lindsborg, Iola, KC, BMD)
- **Default location is automatically marked as accessible** when creating a user
- Accessible checkboxes are optional for other locations
- New users are **active by default**
- Toggle active status in the user list section
- Users are stored locally in JSON format
- This is a separate endpoint from the main Product Finder app
## Adding New Permissions/Columns
The system is designed to be easily extensible. To add new permission columns:
### 1. Update the HTML table
Add a new column header and cells in `user_manager.html`:
```html
<!-- In the table header -->
<th class="checkbox-header" onclick="toggleHeaderCheckbox('newPermission')">
<input type="checkbox" id="headerNewPermission"
onclick="event.stopPropagation(); toggleAllCheckboxes('newPermission')">
Permission Name
</th>
<!-- In each table row -->
<td class="checkbox-cell">
<input type="checkbox" class="newPermission-checkbox"
data-location="LINDS" onchange="updateHeaderCheckbox('newPermission')">
</td>
```
### 2. Update the JavaScript form submission
Modify the form submission handler to collect the new permission:
```javascript
locationSettings[loc.code] = {
active: activeCheckbox ? activeCheckbox.checked : false,
accessible: accessibleCheckbox ? accessibleCheckbox.checked : false,
newPermission: newPermCheckbox ? newPermCheckbox.checked : false // Add this
};
```
### 3. Update the backend (optional)
The backend already handles any properties in `locationSettings`, so no changes are required unless you want validation.
### 4. Reset header checkbox on form submit
Add to the form reset section:
```javascript
document.getElementById('headerNewPermission').checked = false;
document.getElementById('headerNewPermission').indeterminate = false;
```
That's it! The system will automatically save and load the new permission data.
+1
View File
@@ -0,0 +1 @@
web: gunicorn wsgi:app
+262
View File
@@ -0,0 +1,262 @@
PROD_CODE CATEGORY DESCRIPTION
--------------------------------------------------------------------------------
1400 SHPW SERIES 1400 VINYL SLIDING PRIMARY
1400SCR SHPW 1400 SCREEN
1500 SHPT SERIES 1500 S.H. TILT VINYL PRIMARY
1510 FPPW 1510 VINYL INSULATED FIXED LITE
1650 SHPW #1650 INSULATED SINGLE HUNG
1650-10 FPPW #1650-10 INSULATED FIXED LITE
1650SCR SCREENS SCREENS FOR #1650
1650VS SHPW 1650 BOTTOM SASH
1700 SHPW #1700 INSULATED SLIDER
1700CSC SCREENS SCREEN FOR #1700 CENTER VENT SLIDER
1700CV SHPW 1700 CENTER VENT
1700ESC SCREENS SCREEN FOR #1700 ENDS VENT SLIDER
1700EV SHPW 1700 END VENT
1700SCR SCREENS SCREEN FOR #1700 SLIDER
1700VS SHPW #1700 VENT SASH
1710 FPPW C-1710 FIXED LITE
2000 SHPT SERIES 2000 S.H. T.B. TILT PRIMARY
2000SCR SHPT C-2000 SCREEN
2100 SHPT SERIES 2100 THERMAL BREAK SLIDER
2100EV 2000SLI SERIES 2100 3-PANEL SLIDER
2200 FPPW SERIES 2200 T.B. FIXED LITE
2200PDG FPPW SERIES 2200 TB FIXED LITE W/TEMP GLASS
2400 PD 2400 ROYAL CROWN PATIO DOOR
2650 SHPW #2650 SINGLE HUNG SINGLE GLAZED PRIME
265010 FPPW #2650-10 SINGLE GLAZED FIXED LITE
2650SCR SCREENS SCREENS FOR 2650
2650VP SHPW #2650 VENT PANEL - SINGLE HUNGE
2700 SHPW #2700 SINGLE GLAZED SLIDER
2700CV SHPW C-2700 - CENTER VENT
2700EV SHPW C-2700 - END VENT
2700SCR SCREENS SCREENS FOR #2700
2700VS SHPW #2700 VENT SASH
2710 FPPW C-2710 FIXED LITE - SINGLE GLAZED
3000 3000DHP SERIES 3000 D.H. T.B REPLACEMENT WD
3000SCR SCREENS SCREENS FOR #3000
303 STORMS #303 DART STORM WINDOWS
305INS INSERTS #305 SASH WINDCHECK INSERTS
306INS INSERTS #306 SCHLEGEL GLASS INSERTS
3100 3000DHP SERIES 3100 T.B. REPLACEMENT SLIDER
3100EV 3000DHP SERIES 3100 3-PANEL SLIDER
3100SCR SCREENS SCREENS FOR #3100
310PSCR SCRINS #310 PLAIN SCREENS
3200 3000FPP SERIES 3200 T.B. PICTURE WINDOW
3300 CASEMNT 3300 1-PANEL T.B. CASEMENT
3302 CASEMNT 3300 2-PANEL T.B. CASEMENT
3303 CASEMNT 3300 3-PANEL T.B. CASEMENT
3310 CASEMNT 3310 FIXED CASEMENT
3400 CASEMNT 3400 1 PANEL VINYL CLAD CASEMENT
3402 CASEMNT 3400 2 PANEL VINYL CLAD CASEMENT
3403 CASEMNT 3400 3 PANEL VINYL CLAD CASEMENT
350 STORMS #350 ARROW STORM WINDOWS
3500 CASEMNT 3500 1-PANEL WOOD INTERIOR CASEMENT
3502 CASEMNT 3500 2-PANEL WOOD INTERIOR CASEMENT
3503 CASEMNT 3503 3-PANEL WOOD INTERIOR CASEMENT
3700 SHPW #3700 INSULATED SLIDER
3700SCR SCREENS SCREEN FOR #3700 POLE BARN WD
3700VP SHPW #3700 VENT PANEL
3710 FPPW C-3710 INSULATED FIXED LITE
404 STORMS #404 FALCON STORM WINDOWS
404ONE STPW #404 ONE-LITE ST WD
4100 REPLACE SERIES 4100 VINYL SLIDER
450 STORMS #450 RAVEN STORM WINDOWS
450ONE STPW #450 ONE-LITE ST WD
4700 SHPW #4700 SINGLE GLAZED SLIDER
4700SCR SCREENS SCREENS FOR #4700
4710 FPPW #4710 SINGLE GLAZED FIXED LITE
5000SCR SCREENS SCREEN FOR R5000 SLIDER
5200 PD 5200 VINYL PATIO DOORS
5700 SHPW #5700 INSULATED SLIDER
606 STORMS #606 LION STORM WINDOWS
606ONE STPW #606 ONE-LITE ST WD
6100I STD STAR 6100 FULL VIEW STORM DOOR
6100L STD STAR 6100 FULL VIEW STORM DOOR
6300 SHPT SERIES 6300 S.H. TILT VINYL PRIMARY
6301 SHPW SERIES 6301 VINYL SLIDING PRIMARY
650 STORMS #650 LYNX STORM WINDOWS
650ONE STPW #650 ONE-LITE ST WD
6700 SHPW #6700 SINGLE GLAZED SLIDER
7100I STD STAR 7100 FULL VIEW SELF STORING DOOR
7100L STD STAR 7100 FULL VIEW SELF STORING DOOR
808 STORMS 808 HAWK STORM WINDOW
808ONE STPW #808 ONE-LITE ST WD
8100I STD STAR 8100 FULL VIEW STORM DOOR
8100L STD STAR 8100 FULL VIEW STORM DOOR
850 STORMS #850 KENT STORM WINDOWS
850ONE STPW #850 ONE-LITE ST WD
BELMONT 3000FPP BELMONT ALLIANCE DOUBLE HUNG WINDOW
BGI INSERTS BOTTOM GL INSERTS FOR ECONOMY ST WDS
BGI404 INSERTS BOTTOM GL INSERTS FOR #404/450 ST WDS
BGI606 INSERTS BOTTOM GL INSERTS FOR #606/650 ST WDS
BGIST INSERTS INSERTS FOR STORM DOORS
C-500 SHPW C-500 SH THERMAL BREAK INS.
C1150 CASEMNT C1150-VINYL AWNING WINDOW
C1500AR CIR TOP C-1510 VINYL ARCH TOP-OPERATING WINDOW
C1510AR CIR TOP C-1510 VINYL ARCH TOP WINDOW
C1521 CIR TOP C-1521 VINYL CIRCLE TOP
C1526 CIR TOP C-1526 VINYL CIRCLE TOP
C1621 CIR TOP C-1621 INSULATED CIRCLE TOP
C1622 CIR TOP C-1622 INSULATED CIRCLE TOP
C1626VA CIR TOP C-1626VA INSULATED CIRCLE TOPS
C1710 SHPW #1710 PICTURE OVER SLIDER
C1721 CIR TOP C-1721 INSULATED CIRCLE TOPS
C1722 CIR TOP C-1722 INSULATED CIRCLE TOPS
C1724 CIR TOP C-1724 INSULATED CIRCLE TOPS
C1800 C1800 C-1800 INSIDE SLIDING STORM WINDOW
C2021 CIR TOP C-2021 T.B CIRCLE TOPS
C2022 CIR TOP C-2022 T.B. CIRCLE TOPS
C2026 CIR TOP C-2026 T.B. CIRCLE TOPS
C2710 SHPW #2710 PICTURE OVER SLIDER
C300 BSMT C-300 ALUM INSERTS FOR BASEMENT BUCKS
C3221 CIR TOP C-3221 T.B CIRCLE TOPS
C3222 CIR TOP C-3222 T.B. CIRCLE TOPS
C3223 CIR TOP C-3223 T.B. CIRCLE TOPS
C3224 CIR TOP C-3224 T.B. CIRCLE TOPS
C3300 CASEMNT 3300 1-PANEL T.B. CASEMENT
C3700 SHPW C-3700 INSULATED SLIDING POLE BARN WND
C400 BSMT C-400 VINYL INSERT FOR BASEMENT BUCKS
C4000 REPLACE C-4000 SINGLE HUNGE PRIMARY WINDOW
C4260 PD C-4260 STEEL MIRROR DOOR
C500 SHPW C-500 INS THERMAL BREAK SINGLE HUNG
C500GL VENTS LITES OF INSULATED GLASS FOR C-500'S
C500GLP VENTS LITES OF INSULATED GLASS FOR C-500'S
C500PP SHPW C-500 INS THERMAL BREAK SINGLE HUNG
C500SCR SCREENS C-500 SCREEN
C500VP VENTS VENT PANELS FOR C-500
C521 CIR TOP C-521 T.B. CIRCLE TOPS
C522 CIR TOP C-522 T.B. CIRCLE TOPS
C526 CIR TOP C-526 T.B. CIRCLE TOPS
C610 CASEMNT C610 VINYL CASEMENT WINDOW
C620 FPPW C620 VINYL AWNING WINDOWS
C621 CIR TOP C-621 VINYL CIRCLE TOP
C626 CIR TOP C-626 VINYL CIRCLE TOP
C640 FPPW C-640 VINYL FIXED CASEMENT WINDOW
C826 CIR TOP C-826 VINYL CIRCLE TOP
C828 CIR TOP C-828 VINYL CIRCLE TOP
C8321 CIR TOP C-8321 VINYL CIRCLE TOP
C8326 CIR TOP C-8326 VINYL CIRCLE TOP
C900 SHPW C-900 INS THERMAL BREAK SLIDER
C900EV SHPW C-900 ENDS VENT SLIDER
C900SCR SCREENS SCREEN FOR C-900
C910 FPPW C-910 INSULATED T.B. FIXED LITE
C910PP FPPW C-910 ARCH TOP
C921 CIR TOP C-921 T.B. CIRCLE TOPS
C922 CIR TOP C-922 T.B. CIRCLE TOPS
C924 CIR TOP C-924 T.B. CIRCLE TOPS
C931 CIR TOP C-931 T.B. ROUND PRIMARY WINDOW
C939 CIR TOP C-939 T.B. ROUND PRIMARY WINDOW
C940 CIR TOP C-940 T.B. OCTAGON PRIMARY
C949 CIR TOP C949 OCTAGON THERMAL BREAK WINDOW
C960 FPPW C-960 INSULATED T.B. CIRCLE TOP
COBRAI STD COLUMBIA COBRA STORM DOOR
COBRAL STD COLUMBIA COBRA STORM DOOR
COBRATI STD COLUMBIA COBRA STORM DOOR
COBRATL STD COLUMBIA COBRA STORM DOOR
CRWNFVI STD CROWN FULL VIEW STORM DOOR
CRWNFVL STD CROWN FULL VIEW STORM DOOR
CRWNSDI STD COLUMBIA CROWN SCREEN DOOR
CRWNSDL STD COLUMBIA CROWN SCREEN DOOR
D770 SHPT D770 D.H. TILT VINYL PRIMARY WINDOWS
D780 SHPW D780 DOUBLE SLIDE VINYL PRIMARY
D830 SHPT D830 D.H.TILT VINYL PRIMARY WINDOWS
D830SCR SHPT D830 SCREEN
D832 FPPW D832 FIXED VINYL PRIMARY WINDOWS
DSGLASS GLASS DOUBLE STRENGTH GLASS
DURASEA FPPW DURASEAL 5/8" (GRAY) PER REEL
EXPAND EXPANDR SILL EXPANDERS
FULLSCR SCREENS FULL SCREEN
FV10I STD KING FV-10 DECORATOR STORM DOOR
FV10L STD KING FV-10 DECORATOR STORM DOOR
FV3I STD KING FV-3 DECORATOR STORM DOOR
FV3L STD KING FV-3 DECORATOR STORM DOOR
FVGI INSERTS GLASS INSERTS FOR KING ONE LITE
FVSI INSERTS FULL SCREENS ONLY FOR KING ONE-LITES
G3000 GARDEN COLUMBIA COMFORT 3000 VINYL GARDEN WD
GOLIATH STD GOLIATH STORM DOOR
HERCULE STD HERCULES STORM DOOR
IMPERIL PD IMPERIAL PATIO DOORS
INSGLAS GLASS INSULATED GLASS
ISP PD STATIONARY PANELS FOR IMPERIAL DOORS
IVP PD VENT PANELS FOR IMPERIAL PATIO DOORS
JET PD COLUMBIA JET PATIO DOORS
JSP PD STATIONARY PANELS FOR JET DOORS
JVP PD VENT PANELS FOR JET PATIO DOORS
KINGDVI STD KING DUAL VENT STORM DOORS
KINGDVL STD KING DUAL VENT STORM DOORS
KINGFSC INSERTS FULL SCREEN FOR KING ONE-LITE
KINGI STD KING ONE-LITE STORM DOORS
KINGL STD KING ONE-LITE STORM DOORS
KINGSDI STD KING ONE-LITE SCREEN DOOR ONLY
KINGSDL STD KING ONE-LITE SCREEN DOOR ONLY
LINCOLN 3000DHP LINCOLN PRIMARY WOOD WINDOWS
LINCPDR PD LINCOLN FRENCH PATIO DOOR
M1200 PD M1200 PATIO STORM DOOR
M306 INSERTS M-306 SCHLEGEL GLASS INSERTS
OUTSIDE STDMISC OUTSIDE DOOR SWEEPS - ALUM + VINYL
PATIOSC SCREENS PATIO DOOR SCREENS
PDSCRTT SCREENS SPECIAL SIZE SCREENS FOR PATIO DOORS
PRPDS SCREENS SCREENS MADE FROM PLAIN PATIO SCR RAIL
PRSCR PSCREEN SCREENS FOR PRIME MADE FROM #19-88
PRSCR11 PSCREEN SCREEN FOR PRIME MADE FROM #19-11
PSINS INSERTS PLAIN SASH INSERTS
PWS STPW PIN-ON PICTURE WINDOWS
PWSINS INSERTS INSERTS ONLY FOR PIN-ON PICTURE WINDOW
R1150 REPLACE R-1150 VINYL AWNING WINDOWS
R1400 REPLACE SERIES 1400 VINYL SLIDING PRIMARY
R1500 REPLACE SERIES 1500 S.H. TILT VINYL PRIMARY
R1510 FPPW SERIES 1510 FIXED LITE VINYL PRIMARY
R2000 REPLACE SERIES 2000 S.H. T.B. TILT PRIMARY
R2100 REPLACE SERIES 2100 THERMAL BREAK SLIDER
R2100EV R2000SL SERIES 2100 3-PANEL T.B. SLIDER
R2200 RFPPW SERIES 2200 T.B. FIXED LITE
R300 RBSMT C-300 ALUM INSERTS FOR BASEMENT BUCKS
R3302 CASEMNT 3302 2-PANEL T.B. CASEMENT
R400 RBSMT C-400 VINYL INSERT FOR BASEMENT BUCKS
R5000 REPLACE R-5000 INSULATED ALUMINUM SLIDER
R770 REPLACE R770 D.H. TILT VINYL PRIMARY WINDOWS
R770SCR SCREENS FULL SCREEN
R780 REPLACE R780 VINYL SLIDING PRIMARY
R820 REPLACE R820 S.H. TILT VINYL PRIMARY WINDOWS
R821 REPLACE S821 SINGLE SLIDE VINYL PRIMARY
R822 FPPW S822 FIXED VINYL PRIMARY WINDOWS
R830 REPLACE R830 D.H.TILT VINYL PRIMARY WINDOWS
R832 FPPW R832 FIXED VINYL PRIMARY WINDOWS
RCKTRAP INSERTS ROCKET TRAPEZOIDS
REWIRE SCREENS REWIRED SCREENS
RNDROCK INSERTS ROUND ROCKET INSERT
ROCKET INSERTS ROCKET INSERTS
ROYAL STD COLUMBIA ROYAL STORM DOOR
RROCKET INSERTS RADIUS ROCKETS
S820 SHPT S820 S.H. TILT VINYL PRIMARY WINDOWS
S821 SHPW S821 SINGLE SLIDE VINYL PRIMARY
S822 FPPW S822 FIXED VINYL PRIMARY WINDOWS
SCRI INSERTS SCREEN INSERTS FOR ECONOMY STORM WDS
SCRI404 INSERTS SCREEN INSERTS FOR #404/450 STORM WDS
SCRI606 INSERTS SCREEN INSERTS FOR #606/650 STORM WDS
SCRI808 INSERTS SCREEN INSERTS FOR #808/850 STORM WIND
SS10I STD COBRA SS-10 DECORATOR STORM DOOR
SS10L STD COBRA SS-10 DECORATOR STORM DOOR
SS3I STD COBRA SS-3 DECORATOR STORM DOOR
SS3L STD COBRA SS-3 DECORATOR STORM DOOR
SSGLASS GLASS SINGLE STRENGTH GLASS
SSSCR INSERTS SCREEN INSERT FOR SELF STORING DOORS
TBGI INSERTS TEMPERED GLASS INSERTS FOR SS DOORS
TBR PD IMPERIAL T.B.R. REPLACEMENT PATIO DOOR
TGI INSERTS TOP GL INSERTS FOR ECONOMY ST WDS
TGI404 INSERTS TOP GLASS INSERTS FOR #404/450 ST WDS
TGI606 INSERTS TOP GLASS INSERTS FOR #606/650 ST WDS
TGIODD INSERTS TOP GLASS INSERTS FOR STORM DOORS
THOR STD THOR - STORM DOOR
TIARAI STD COLUMBIA TIARA SELF STORING STORM DOOR
TIARAL STD COLUMBIA TIARA SELF STORING STORM DOOR
TVGROOV INSERTS TEMPERED V-GROOVE ONE-LITE INSERTS
TVI STD COLUMBIA COBRA TWIN VENT STORM DOOR
TVL STD COLUMBIA COBRA TWIN VENT STORM DOOR
VKI STOCK VENTILATOR KICKPANEL FOR KING ONELITE
VKS STOCK VENTILATOR SCREEN FOR KING ONE LITE
VP3700 VENTS VENT PANELS FOR #3700
WINDGAT VACW ALLIANCE WINDGATE CASEMENT WINDOW
XBUCKIN INSERTS TEMPERED GLASS INSERTS FOR CROSSBUCKS
ZBARS STDMISC Z-BARS FOR STORM DOORS
+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Details Form</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 20px;
border: 2px solid #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
input[type="text"],
input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
}
textarea {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
button {
width: 100%;
padding: 12px;
background-color: #d0d0ff;
border: 1px solid #333;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-bottom: 15px;
}
button:hover {
background-color: #b0b0ff;
}
.specifications-section {
border: 3px solid #4CAF50;
padding: 15px;
margin-bottom: 15px;
background-color: #f0fff0;
}
.specifications-section.hidden {
display: none;
}
.conditional-field {
display: none;
}
.conditional-field.visible {
display: block;
}
select {
width: 100%;
padding: 8px;
border: 1px solid #333;
font-size: 14px;
}
.spec-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.spec-row label {
margin-bottom: 0;
}
.spec-row input {
border: 1px solid #333;
padding: 6px;
}
.questions-section {
border: 1px solid #333;
padding: 15px;
text-align: center;
background-color: #fafafa;
}
.questions-section p {
margin-bottom: 10px;
font-size: 12px;
}
.questions-section .placeholder {
font-style: italic;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="form-group">
<label>Type (door, window, other)</label>
<select id="typeSelect" onchange="toggleOtherField()">
<option value="">-- Select Type --</option>
<option value="door">Door</option>
<option value="window">Window</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group conditional-field" id="otherField">
<label>Please specify</label>
<input type="text" placeholder="Enter type details">
</div>
<div class="form-group">
<label>Description</label>
<textarea placeholder="textfield"></textarea>
</div>
<div class="form-group">
<label>Notes</label>
<textarea placeholder="textfield"></textarea>
</div>
<button onclick="toggleSpecifications()">Match and Review</button>
<div class="specifications-section hidden" id="specificationsSection">
<div class="spec-row">
<label>Product Code:</label>
<input type="text" placeholder="" readonly>
</div>
<div class="spec-row">
<label>Width:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Height</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Color</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Frame:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Screen:</label>
<input type="text" placeholder="">
</div>
<div class="spec-row">
<label>Etc...</label>
<input type="text" placeholder="">
</div>
</div>
<div class="form-group">
<label>Quantity</label>
<input type="number" min="1" value="1" placeholder="">
</div>
<button>Confirm and Add</button>
<div class="questions-section">
<p>Possible questions for customers based on what is selected or missing</p>
<div class="placeholder">[list - checkbox per question]</div>
</div>
</div>
<script>
function toggleSpecifications() {
const section = document.getElementById('specificationsSection');
section.classList.toggle('hidden');
}
function toggleOtherField() {
const typeSelect = document.getElementById('typeSelect');
const otherField = document.getElementById('otherField');
if (typeSelect.value === 'other') {
otherField.classList.add('visible');
} else {
otherField.classList.remove('visible');
}
}
</script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Finder Quiz</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>Product Finder</h1>
</div>
<div class="breadcrumb" id="breadcrumb">
Start
</div>
<div id="content">
<!-- Content will be dynamically loaded here -->
</div>
</div>
<script src="js/script.js"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
@echo off
echo ====================================
echo Product Finder Quiz - Flask App
echo ====================================
echo.
echo Checking Python installation...
python --version
if errorlevel 1 (
echo ERROR: Python is not installed or not in PATH
echo Please install Python 3.8 or higher
pause
exit /b 1
)
echo.
echo Installing dependencies...
pip install -r requirements.txt
if errorlevel 1 (
echo ERROR: Failed to install dependencies
pause
exit /b 1
)
echo.
echo Starting Flask application...
echo.
echo Application will be available at:
echo http://localhost:8080/quiz
echo.
echo Press Ctrl+C to stop the server
echo.
python app.py
pause
+13
View File
@@ -0,0 +1,13 @@
https://columbiawindows.com/product-detail/cobra-aluminum-storm-doors-1100/
https://columbiawindows.com./wp-content/uploads/2014/10/How-to-Measure2.pdf
HINGE LOCATION:
Face your door opening from outside the house. If hinges are needed on the left
side, specify a left hinge door. If hinges are needed on the right, specify a right
hinge door
+106
View File
@@ -0,0 +1,106 @@
[
{
"id": "404",
"productCode": "404",
"category": "STORMS",
"description": "#404 FALCON STORM WINDOWS",
"discontinued": false,
"location": "Iola",
"baseType": "Window",
"subType": {
"door": null,
"window": "Storm Window"
},
"materials": [
"Aluminum"
],
"colors": [
"Black",
"White",
"Bronze",
"Sandstone"
],
"isAccessory": false,
"compatibleAccessories": [],
"image": "images/404.jpg",
"imageConfig": {
"layered": true,
"basePath": "images/products/404/",
"layers": {
"base": "base.jpg",
"door": {
"white": "door-white.png",
"black": "door-black.png",
"bronze": "door-bronze.png",
"sandstone": "door-sandstone.png"
},
"hardware": "handle.png"
}
}
},
{
"id": "450",
"productCode": "450",
"category": "STORMS",
"description": "#450 RAVEN STORM WINDOWS",
"discontinued": false,
"location": "Iola",
"baseType": "Window",
"subType": {
"door": null,
"window": "Storm Window"
},
"materials": [
"Aluminum"
],
"colors": [
"Black",
"Bronze"
],
"isAccessory": false,
"compatibleAccessories": [],
"image": "images/450.jpg"
},
{
"id": "505",
"productCode": "505",
"category": "DOORS",
"description": "#505 STORM DOOR",
"discontinued": false,
"location": "Iola",
"baseType": "Door",
"subType": {
"door": "Storm Door",
"window": null
},
"materials": [
"Aluminum",
"Vinyl"
],
"colors": [
"White",
"Black",
"Bronze",
"Tan"
],
"isAccessory": false,
"compatibleAccessories": ["ACC-001", "ACC-002"],
"image": "images/505.jpg",
"imageConfig": {
"layered": true,
"basePath": "images/products/505/",
"layers": {
"base": "base.jpg",
"door": {
"white": "door-white.png",
"black": "door-black.png"
},
"hardware": "handle-lever.png",
"overlay": {
"inside": "view-inside.png",
"outside": "view-outside.png"
}
}
}
}
]
+239
View File
@@ -0,0 +1,239 @@
# Product Encoding System
## Overview
This document describes the hex-based encoding system for creating stable, bookmarkable URLs for product configurations. The system uses bit-packing to create compact strings that encode product type, color, material, and hardware options.
## Design Goals
1. **Stable URLs**: Old bookmarks continue to work even as new options are added
2. **Compact**: Keep URLs under 32-64 characters
3. **Extensible**: Reserve space for future expansion
4. **Reversible**: Each encoded string can be uniquely decoded back to its components
5. **Version-aware**: Support future format changes without breaking old URLs
## Encoding Format
```
Format: v-T-C-M-HHHH-HHHH-HHHH
│ │ │ │ │ │ └── Hardware 3 (optional)
│ │ │ │ │ └─────── Hardware 2 (optional)
│ │ │ │ └──────────── Hardware 1
│ │ │ └────────────── Material (1 hex char, 0-F)
│ │ └──────────────── Color (1 hex char, 0-F)
│ └────────────────── Type (1 hex char, 0-F)
└──────────────────── Version (1 hex char, 0-F)
Example: "0-1-4-1-0D94"
- Version: 0 (using 5-bit encoding per field)
- Type: 1 (Patio Door)
- Color: 4 (White)
- Material: 1 (Aluminum)
- Hardware 1: 0D94 (Lever, Standard, Brass)
Max Length: ~24 characters (with 3 hardware items)
```
## Version 0 Encoding (Current)
### Main Product Attributes (Single Hex Character Each)
Each main attribute uses a single hex character (0-F = 0-15):
#### Type Values (1 hex char)
```
1 = Patio Door
2 = Reserved
3 = Storm Door
4 = Reserved
5 = Storm Window
6 = Reserved
7 = Primary Window
8-F = Reserved (8 slots for future door/window types)
```
#### Color Values (1 hex char)
```
1 = Black
2 = Bronze
3 = Sandstone
4 = White
5 = Tan
6 = Mill
7-F = Reserved (9 slots for future colors)
```
#### Material Values (1 hex char)
```
1 = Aluminum
2 = Vinyl
3-F = Reserved (13 slots for future materials)
```
### Hardware Encoding (4 Hex Characters = FFFF)
Each hardware item uses **4 hex characters** (16 bits) split into fields using **5 bits per field**:
```
16 bits total:
- Bits 10-14: Type (5 bits, 0-31 values)
- Bits 5-9: Style (5 bits, 0-31 values)
- Bits 0-4: Color (5 bits, 0-31 values)
- Bit 15: Reserved (1 bit)
```
#### Hardware Type Values (5 bits = 0-31)
```
0 = None/Not Set
1 = Lever
2 = Pull
3 = Pull Handle
4 = Deadbolt
5 = Hinge
6-31 = Reserved (26 slots)
```
#### Hardware Style Values (5 bits = 0-31)
```
0 = None/Not Set
1 = Standard
2 = Push
3 = Alternative
4 = Contemporary
5 = Traditional
6-31 = Reserved (26 slots)
```
#### Hardware Color Values (5 bits = 0-31)
```
0 = None/Not Set
1 = Brass
2 = White
3 = Black
4 = Satin
5 = Nickel
6 = Bronze
7-31 = Reserved (25 slots)
```
## Bit Math Examples
### Encoding Hardware
```
Example: Lever, Standard, Brass
- Type: 1 (Lever)
- Style: 1 (Standard)
- Color: 1 (Brass)
Binary calculation:
Type (1) = 00001 (bits 10-14)
Style (1) = 00001 (bits 5-9)
Color (1) = 00001 (bits 0-4)
Combined: 0000010000100001 = 0x0421
Hex: "0421"
```
```
Example: Pull Handle, Alternative, Satin
- Type: 3 (Pull Handle)
- Style: 3 (Alternative)
- Color: 4 (Satin)
Binary calculation:
Type (3) = 00011 (bits 10-14)
Style (3) = 00011 (bits 5-9)
Color (4) = 00100 (bits 0-4)
Combined: 0000110001100100 = 0x0C64
Hex: "0C64"
```
### Decoding Hardware
```
Given hex: "0D94"
Binary: 0000110110010100
Extract fields:
Type = bits 10-14 = 00011 = 3 (Pull Handle)
Style = bits 5-9 = 01100 = 12
Color = bits 0-4 = 10100 = 20
```
## URL Examples
### Simple Products (No Hardware)
```
"0-1-4-1" = Patio Door, White, Aluminum
"0-3-4-1" = Storm Door, White, Aluminum
"0-7-1-1" = Primary Window, Black, Aluminum
"0-5-2-2" = Storm Window, Bronze, Vinyl
```
### Products with Hardware
```
"0-3-4-1-0421" = Storm Door, White, Aluminum, Lever-Standard-Brass
"0-3-1-1-0421-0C64" = Storm Door, Black, Aluminum,
Hardware1: Lever-Standard-Brass,
Hardware2: Pull Handle-Alternative-Satin
```
## Expansion Strategy
### Adding New Options (Compatible)
When adding new options within existing capacity (0-15 for main attributes, 0-31 for hardware fields):
```
Old system: Color 1-6 defined, 7-15 reserved
New system: Add Color 7 = Charcoal
Old URLs still work: "0-1-4-1" still means Patio Door, White, Aluminum
New URLs use new values: "0-1-7-1" means Patio Door, Charcoal, Aluminum
```
### Version 1 (Future Expansion)
If we exceed 16 main attribute values or 32 hardware field values:
```
Version 1: Use 6 bits per hardware field (64 values each)
Format: "1-TT-CC-MM-HHHHH-HHHHH"
Changes:
- Main attributes expand to 2 hex chars each (256 values)
- Hardware expands to 5 hex chars each (18 bits = 6 bits per field)
```
## Compatibility Rules
1. **Never reuse reserved values** until a new version is released
2. **Never change existing value meanings** (e.g., don't make "1" mean something else)
3. **Always decode based on version character**
4. **Maintain lookup tables** for each version
## Implementation Notes
- Use bit-shifting operators (`<<`, `>>`) for performance
- Use bitwise AND (`&`) with masks to extract fields
- Pad hex strings with leading zeros to fixed width
- Validate decoded values against valid ranges
- Return error for unknown version codes
## Benefits
**Stable**: Values 0-15 remain the same even if we expand to 0-31
**Compact**: 13-24 characters for most products
**Readable**: Hex is human-debuggable
**Efficient**: Single parse operation, no string splitting
**Extensible**: Version prefix allows future format changes
**Reliable**: Bit operations are deterministic and reversible
---
**Last Updated**: March 26, 2026
**Current Version**: 0 (5-bit hardware encoding)
+505
View File
@@ -0,0 +1,505 @@
# Encoding Table Generation System
## Problem Statement
Currently, encoding lookup tables require manual maintenance of both forward and reverse mappings:
```javascript
// Must maintain both of these manually
const PRODUCT_COLOR = {
'BLACK': 1,
'BRONZE': 2,
// ... add new color here
};
const PRODUCT_COLOR_REVERSE = {
1: 'BLACK',
2: 'BRONZE',
// ... and remember to add it here too!
};
```
**Issues:**
- ❌ Prone to human error (forgetting reverse mapping)
- ❌ Risk of mismatches between forward/reverse
- ❌ No validation for duplicate values
- ❌ No documentation of available slots
- ❌ Difficult for non-technical users to add values
## Proposed Solution: Python-Generated Tables
### Architecture
```
User edits simple source file
Python validation script
Generate JavaScript + Python constants
Auto-served by Flask (or manual build step)
```
### File Structure
```
app/
data/
encoding_values.json (or .yaml) ← USER EDITS THIS
js/
encoding_tables.js ← AUTO-GENERATED
generate_encoding_tables.py ← GENERATOR SCRIPT
```
## Source File Format
### Option 1: JSON with Comments Support
**`app/data/encoding_values.json`**
```json
{
"metadata": {
"version": 0,
"description": "Product encoding value definitions"
},
"tables": {
"product_type": {
"max_value": 15,
"bit_size": 4,
"values": [
{"name": "PATIO_DOOR", "value": 1, "display": "Patio Door"},
{"name": "STORM_DOOR", "value": 3, "display": "Storm Door"},
{"name": "STORM_WINDOW", "value": 5, "display": "Storm Window"},
{"name": "PRIMARY_WINDOW", "value": 7, "display": "Primary Window"}
]
},
"product_color": {
"max_value": 15,
"bit_size": 4,
"values": [
{"name": "BLACK", "value": 1, "display": "Black"},
{"name": "BRONZE", "value": 2, "display": "Bronze"},
{"name": "SANDSTONE", "value": 3, "display": "Sandstone"},
{"name": "WHITE", "value": 4, "display": "White"},
{"name": "TAN", "value": 5, "display": "Tan"},
{"name": "MILL", "value": 6, "display": "Mill"}
]
},
"product_material": {
"max_value": 15,
"bit_size": 4,
"values": [
{"name": "ALUMINUM", "value": 1, "display": "Aluminum"},
{"name": "VINYL", "value": 2, "display": "Vinyl"}
]
},
"hardware_type": {
"max_value": 31,
"bit_size": 5,
"values": [
{"name": "NONE", "value": 0, "display": "None"},
{"name": "LEVER", "value": 1, "display": "Lever"},
{"name": "PULL", "value": 2, "display": "Pull"},
{"name": "PULL_HANDLE", "value": 3, "display": "Pull Handle"},
{"name": "DEADBOLT", "value": 4, "display": "Deadbolt"},
{"name": "HINGE", "value": 5, "display": "Hinge"}
]
},
"hardware_style": {
"max_value": 31,
"bit_size": 5,
"values": [
{"name": "NONE", "value": 0, "display": "None"},
{"name": "STANDARD", "value": 1, "display": "Standard"},
{"name": "PUSH", "value": 2, "display": "Push"},
{"name": "ALTERNATIVE", "value": 3, "display": "Alternative"},
{"name": "CONTEMPORARY", "value": 4, "display": "Contemporary"},
{"name": "TRADITIONAL", "value": 5, "display": "Traditional"}
]
},
"hardware_color": {
"max_value": 31,
"bit_size": 5,
"values": [
{"name": "NONE", "value": 0, "display": "None"},
{"name": "BRASS", "value": 1, "display": "Brass"},
{"name": "WHITE", "value": 2, "display": "White"},
{"name": "BLACK", "value": 3, "display": "Black"},
{"name": "SATIN", "value": 4, "display": "Satin"},
{"name": "NICKEL", "value": 5, "display": "Nickel"},
{"name": "BRONZE", "value": 6, "display": "Bronze"}
]
}
}
}
```
### Option 2: YAML (Better for Comments)
**`app/data/encoding_values.yaml`**
```yaml
version: 0
description: Product encoding value definitions
tables:
product_type:
max_value: 15
bit_size: 4
values:
- name: PATIO_DOOR
value: 1
display: Patio Door
- name: STORM_DOOR
value: 3
display: Storm Door
# Reserved: 2, 4, 6, 8-15
product_color:
max_value: 15
bit_size: 4
values:
- name: BLACK
value: 1
display: Black
- name: BRONZE
value: 2
display: Bronze
# ... etc
# Reserved: 7-15 (9 slots available)
```
## User Workflow
### Adding a New Color
**Before (Manual):**
1. Open `encoding_helper.js`
2. Find `PRODUCT_COLOR` object
3. Add new entry with unused value
4. Find `PRODUCT_COLOR_REVERSE` object
5. Add matching reverse entry
6. Hope you didn't make a mistake
**After (Generated):**
1. Open `encoding_values.json`
2. Add one line: `{"name": "CHARCOAL", "value": 7, "display": "Charcoal"}`
3. Run `python generate_encoding_tables.py` (or auto-regenerated by Flask)
4. Done! JavaScript and Python constants updated
## Generator Script
**`app/generate_encoding_tables.py`**
```python
#!/usr/bin/env python3
"""
Generate JavaScript and Python encoding tables from source JSON/YAML.
Usage:
python generate_encoding_tables.py
Or add to Flask app for auto-generation on file change.
"""
import json
import os
from datetime import datetime
from pathlib import Path
def load_source_file(filepath):
"""Load encoding values from JSON or YAML."""
with open(filepath, 'r') as f:
if filepath.endswith('.yaml') or filepath.endswith('.yml'):
import yaml
return yaml.safe_load(f)
else:
return json.load(f)
def validate_table(table_name, table_data):
"""Validate a single table for errors."""
errors = []
max_value = table_data['max_value']
values = table_data['values']
used_values = set()
used_names = set()
for item in values:
name = item['name']
value = item['value']
# Check for duplicate values
if value in used_values:
errors.append(f"{table_name}: Duplicate value {value}")
used_values.add(value)
# Check for duplicate names
if name in used_names:
errors.append(f"{table_name}: Duplicate name '{name}'")
used_names.add(name)
# Check value range
if value < 0 or value > max_value:
errors.append(f"{table_name}: Value {value} out of range (0-{max_value})")
# Validate naming convention (uppercase with underscores)
if not name.isupper() or not name.replace('_', '').isalpha():
errors.append(f"{table_name}: Invalid name format '{name}' (use UPPERCASE_WITH_UNDERSCORES)")
# Report available slots
available = sorted(set(range(max_value + 1)) - used_values)
info = f"{table_name}: {len(used_values)}/{max_value + 1} slots used, {len(available)} available"
if available and len(available) <= 10:
info += f" {available}"
return errors, info
def generate_javascript(data, output_path):
"""Generate JavaScript constants file."""
lines = [
"// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY",
"// Generated from: app/data/encoding_values.json",
f"// Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"//",
"// To add new values:",
"// 1. Edit app/data/encoding_values.json",
"// 2. Run: python app/generate_encoding_tables.py",
"// 3. Restart Flask server (or it auto-reloads)",
"",
"// ============================================================================",
"// ENCODING CONSTANTS",
"// ============================================================================",
"",
f"const ENCODING_VERSION = {data['metadata']['version']};",
""
]
for table_name, table_data in data['tables'].items():
const_name = table_name.upper()
# Generate forward mapping
lines.append(f"// {const_name} (max: {table_data['max_value']}, bits: {table_data['bit_size']})")
lines.append(f"const {const_name} = {{")
for item in table_data['values']:
lines.append(f" '{item['name']}': {item['value']},")
lines.append("};")
lines.append("")
# Generate reverse mapping
lines.append(f"const {const_name}_REVERSE = {{")
for item in table_data['values']:
lines.append(f" {item['value']}: '{item['name']}',")
lines.append("};")
lines.append("")
# Generate display mapping
lines.append(f"const {const_name}_DISPLAY = {{")
for item in table_data['values']:
lines.append(f" '{item['name']}': '{item['display']}',")
lines.append("};")
lines.append("")
# Write file
with open(output_path, 'w') as f:
f.write('\n'.join(lines))
print(f"✓ Generated: {output_path}")
def generate_python(data, output_path):
"""Generate Python constants file (optional)."""
lines = [
'"""',
"AUTO-GENERATED FILE - DO NOT EDIT MANUALLY",
"Generated from: app/data/encoding_values.json",
f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
'"""',
"",
f"ENCODING_VERSION = {data['metadata']['version']}",
""
]
for table_name, table_data in data['tables'].items():
const_name = table_name.upper()
lines.append(f"# {const_name} (max: {table_data['max_value']}, bits: {table_data['bit_size']})")
lines.append(f"{const_name} = {{")
for item in table_data['values']:
lines.append(f" '{item['name']}': {item['value']},")
lines.append("}")
lines.append("")
lines.append(f"{const_name}_REVERSE = {{")
for item in table_data['values']:
lines.append(f" {item['value']}: '{item['name']}',")
lines.append("}")
lines.append("")
with open(output_path, 'w') as f:
f.write('\n'.join(lines))
print(f"✓ Generated: {output_path}")
def generate_documentation(data, output_path):
"""Generate markdown documentation."""
lines = [
"# Encoding Values Reference",
"",
f"**Version:** {data['metadata']['version']} ",
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ",
"",
"## Overview",
"",
"This document lists all currently defined encoding values.",
"",
"## Quick Reference",
""
]
for table_name, table_data in data['tables'].items():
lines.append(f"### {table_name.replace('_', ' ').title()}")
lines.append("")
lines.append(f"**Capacity:** {len(table_data['values'])}/{table_data['max_value'] + 1} slots used ")
lines.append(f"**Bit Size:** {table_data['bit_size']} bits ")
lines.append("")
lines.append("| Value | Name | Display |")
lines.append("|-------|------|---------|")
for item in sorted(table_data['values'], key=lambda x: x['value']):
lines.append(f"| {item['value']} | `{item['name']}` | {item['display']} |")
lines.append("")
with open(output_path, 'w') as f:
f.write('\n'.join(lines))
print(f"✓ Generated: {output_path}")
def main():
"""Main generation function."""
base_dir = Path(__file__).parent
source_file = base_dir / 'data' / 'encoding_values.json'
# Load source data
print(f"Loading: {source_file}")
data = load_source_file(source_file)
# Validate all tables
print("\nValidating tables...")
all_errors = []
for table_name, table_data in data['tables'].items():
errors, info = validate_table(table_name, table_data)
print(f" {info}")
all_errors.extend(errors)
if all_errors:
print("\n❌ VALIDATION ERRORS:")
for error in all_errors:
print(f" - {error}")
return 1
print("\n✓ All tables valid")
# Generate files
print("\nGenerating files...")
generate_javascript(data, base_dir / 'js' / 'encoding_tables.js')
generate_python(data, base_dir / 'encoding_tables_constants.py')
generate_documentation(data, base_dir.parent / 'planning' / 'ENCODING_VALUES_REFERENCE.md')
print("\n✓ Generation complete!")
return 0
if __name__ == '__main__':
exit(main())
```
## Flask Integration (Auto-Generation)
Add to `app.py`:
```python
import os
from datetime import datetime
@app.before_request
def check_encoding_tables():
"""Auto-regenerate encoding tables if source has changed."""
source_file = 'data/encoding_values.json'
output_file = 'js/encoding_tables.js'
# Check if regeneration needed
if not os.path.exists(output_file):
print("Encoding tables not found, generating...")
os.system('python generate_encoding_tables.py')
else:
source_mtime = os.path.getmtime(source_file)
output_mtime = os.path.getmtime(output_file)
if source_mtime > output_mtime:
print("Encoding values changed, regenerating tables...")
os.system('python generate_encoding_tables.py')
```
## Benefits
### For Non-Technical Users
- ✅ Edit simple JSON file (no JavaScript knowledge needed)
- ✅ Clear structure with examples
- ✅ Automatic validation catches errors
- ✅ Can't create mismatches between forward/reverse
### For Developers
- ✅ Single source of truth
- ✅ Auto-generated documentation
- ✅ Validation prevents bugs
- ✅ Easy to add new tables
- ✅ Can generate for multiple targets (JS, Python, docs)
### For Maintenance
- ✅ Version control friendly (one file to track)
- ✅ Easy to review changes (just JSON diff)
- ✅ Reports available slots automatically
- ✅ Enforces naming conventions
## Migration Path
### Phase 1: Create Source File
1. Extract existing values to `encoding_values.json`
2. Test generator script
3. Verify output matches current `encoding_helper.js`
### Phase 2: Integrate Generator
1. Add generator script to project
2. Update build process / Flask app
3. Test auto-generation
### Phase 3: Switch to Generated Tables
1. Update `encoding_helper.js` to import generated file
2. Remove manual mappings
3. Update documentation
### Phase 4: User Documentation
1. Create guide for adding new values
2. Document validation rules
3. Add examples
## Future Enhancements
- Web UI for editing values (no need to edit JSON directly)
- Export to CSV for client review
- Import from Excel/CSV
- Visual capacity indicators
- Conflict detection across tables
- Automatic value assignment (find next available slot)
## Related Files
- [ENCODING_SYSTEM.md](ENCODING_SYSTEM.md) - Technical specification
- `app/js/encoding_helper.js` - Current manual implementation
- `app/js/encoding_tables.js` - Future generated file
---
**Status:** Planning
**Priority:** Medium
**Effort:** ~4-6 hours implementation + testing
**Dependencies:** None
File diff suppressed because it is too large Load Diff
+208
View File
@@ -0,0 +1,208 @@
# Product Finder - Project Features Summary
**Date:** March 25, 2026
**Project:** Product Finder Application for Columbia Metals
**Participants:** Jesse Salmon, Jason Soltys, Darlene Lampe
---
## Core Application Features
### 1. Product Selection Workflow
- **Multi-step navigation system** using button-based interface
- **Goal:** Minimize text entry - "tap, tap, tap" workflow
- **JSON-driven configuration** for flexible navigation without code changes
- Sequential selection process: Color → Size → Model/Type → Accessories
### 2. Color Selection
- **Visual color buttons** (similar to current implementation showing Black, Bronze, Mill, etc.)
- Filters available products based on color selection
- **Mill finish to be removed** (not available for any door)
- Default hardware colors match door colors (e.g., bronze door = black pull, white door = white pull)
### 3. Size Selection System
#### Standard Sizes
- **Three standard sizes for storm doors:**
- 2668 (26" x 68")
- 2868 (28" x 68")
- 3068 (36" x 81")
- Display both nominal size (e.g., "3068") and actual dimensions (e.g., "36 x 81")
- **Button-based interface** (similar to color selection)
- Standard sizes auto-populate width and height fields (grayed out/non-editable)
#### Custom Sizes
- **Custom option button** reveals width/height input fields
- **Storm doors:** Opening size ONLY (no tip-to-tip)
- Display warning in RED: "Opening size only - not tip to tip"
- **Patio doors:** Either opening size or tip-to-tip allowed
- Toggle between standard size buttons and custom input
### 4. Product Categories
#### Storm Doors
- **Models:** Cobra, King, King Dual Vent
- Category filtering based on selected criteria
- Product images for each model
- **Measurement requirement:** Opening size only for custom orders
#### Patio Doors
- Standard size: 5069
- Can use opening size or tip-to-tip for custom
#### Products to Exclude
- Parts (Z-bars, inserts)
- Discontinued items (marked in database)
- M1200 patio storm door
- Storm door inserts
### 5. Hardware & Accessories
#### Hardware Selection
- **Types:** Brass lever, Satin silver, Black pull, White pull
- Labeled as "Hardware" in CGW system
- Default hardware based on door color
- Optional customization via dropdown
#### Hinge Side Selection
- **Required field** - no default setting
- Options: Left hand or Right hand
- **Measurement instruction:** "Facing the door from the outside, which side are the hinges on?"
- Visual prompt/popup to ensure customer understands measurement perspective
### 6. Visual Elements
#### Product Images
- Display product photos for each door model
- **Door image flipping:** Mirror image based on hinge side selection (JavaScript/CSS)
- Images sourced from website and database
#### Outside View Indicator
- **Proposed design:** Doghouse illustration with:
- Small roof/overhang
- Plant or bush beside door
- Wall indication
- Purpose: Help customers understand they're viewing from outside
- Simple, rough design acceptable
#### Navigation Elements
- Button-based interface throughout
- Visual color swatches
- Product thumbnails
- Continue/Next buttons
### 7. Data Management
#### Database Integration
- **Source:** CGW (Columbia Gateway Windows) database
- **Locations:** Iola, Kansas City, Lindsberg
- Google Sheets for product data management
- Product codes exported from CGW
#### Data Filtering
- Filter discontinued items (marked in spreadsheet)
- Remove accessories from main product list
- **Column H (Door Type/Subtype):** Delete to remove from product list
- **Columns J & K:** Accessory/hardware flags
#### Product Naming Consistency
- Challenge: Same products have different model numbers across locations
- **Solution:** Use catalog names (customer-facing)
- Internal CGW names vary by plant
- Need standardization project for matching products across locations
### 8. User Experience Features
#### Add to Cart/Order
- Button to add selected product to sales order
- Appears after all selections made
- Label: "Add to [cart/sales order/hardware]"
#### Measurement Instructions
- **Link to PDF** with measurement guidelines
- Accessible from size selection screen
- Shows how to determine hinge side, measure openings, etc.
#### Validation & Warnings
- Opening size requirement for custom storm doors (highlighted in red)
- Required field indicators (hinge side selection)
- Visual cues for defaults vs. custom selections
### 9. Architecture & Technical Approach
#### Navigation System
- JSON file manages all navigation flow
- Changes made via JSON manipulation (no code changes needed)
- Flexible architecture to support iteration
- **Current limitation:** Toggle functionality requires code changes
#### Development Philosophy
- Hard-code initial features for rapid iteration
- Avoid premature optimization
- Build architecture after understanding full requirements
- Keep front-end decoupled from back-end structure
### 10. Future Considerations
#### Windows
- Primary windows and storm windows have different measurement rules
- "Whole different ball game" - to be addressed later
#### Parts/Accessories Integration
- Parts sheet for storm door inserts
- Separate handling from complete products
- Discontinued items may need parts availability
#### Multi-Location Support
- Consolidate product naming across plants
- Maintain location-specific inventory
- Unified catalog names for customers
---
## Key Business Rules
1. **Storm doors (custom):** Opening size only
2. **Patio doors (custom):** Opening size OR tip-to-tip
3. **Standard sizes:** Auto-populate dimensions (read-only)
4. **Hinge side:** Required selection with instructional prompt
5. **Hardware:** Defaults based on color, optional customization
6. **Discontinued items:** Filter from product display
7. **Parts/accessories:** Exclude from main product categories
---
## Implementation Priorities
### High Priority
1. Size selection with standard sizes as buttons
2. Remove discontinued items from display
3. Hinge side selection with flip image functionality
4. Hardware dropdown with defaults
5. Custom size with opening-size-only warning for storm doors
### Medium Priority
1. Outside view visual indicator (doghouse design)
2. Link to measurement instructions PDF
3. Add to cart/order button
4. Product naming standardization across locations
### Low Priority (Future)
1. Windows integration
2. Parts/accessories handling
3. Advanced filtering options
4. Multi-location inventory management
---
## Quality & Accuracy Concerns
- **Arnie's primary concern:** Accuracy of the system
- Emphasis on preventing measurement errors
- Visual aids to reduce confusion
- Clear labeling and validation messages
- "It's too complicated" - need to prove simplicity through good UX
---
*This summary was generated from the March 25, 2026 project planning meeting transcript.*
Binary file not shown.
+147
View File
@@ -0,0 +1,147 @@
# Product App - Modular Flask Application
## 🏗️ Structure
```
product_app/
├── app.py # Main application file
├── passenger_wsgi.py # Passenger WSGI entry point for production
├── requirements.txt # Python dependencies
├── blueprints/ # Modular routes organized by feature
│ ├── __init__.py
│ ├── auth.py # Login/logout/session management
│ └── users.py # User CRUD operations
├── templates/ # HTML templates
│ ├── login.html
│ └── user_manager.html
└── data/ # Data storage
└── users.json # User accounts with hashed passwords
```
## 🎯 Features Implemented
### ✅ Authentication System (blueprints/auth.py)
- Login with username/password
- Session management
- Password hashing (PBKDF2-SHA256, 1M iterations)
- Login required decorator
- Permission checking: `can_user('manage_users')`
- User status checking (active/inactive)
### ✅ User Management (blueprints/users.py)
- View all users
- Add new users
- Delete users
- Toggle active/inactive status
- Change passwords (requires admin verification)
- Location-based access control
- Permission system (manage_users, create_quotes, etc.)
- Download users as JSON
## 🔐 Default Login
- **Username:** `Master`
- **Password:** `Master`
- **Permissions:** Full access (manage_users)
## 🚀 Local Testing
```bash
cd product_app
python app.py
```
Visit: http://localhost:8080/
## Routes
### Main Routes
- `/` - Home (redirects to login if not authenticated)
- `/test` - Test page to verify app is running
### Authentication Routes (auth_bp)
- `/login` - Login page
- `/logout` - Logout
- `/api/login` - POST: Login API
- `/api/session` - GET: Current session info
### User Management Routes (users_bp)
- `/users/` - User management page
- `/users/api` - GET: List all users, POST: Add user
- `/users/api/<index>` - DELETE: Delete user
- `/users/api/<index>/active` - PATCH: Toggle active status
- `/users/api/<index>/change-password` - POST: Change password
- `/users/api/download` - GET: Download users.json
## 📦 Production Deployment
### Upload to Server: `/home/bmdwtjuw/product-finder/`
Files to upload:
- `app.py`
- `passenger_wsgi.py` (or just use existing)
- `blueprints/` (entire folder)
- `__init__.py`
- `auth.py`
- `users.py`
- `templates/` (entire folder)
- `login.html`
- `user_manager.html`
- `data/users.json`
### Control Panel Settings
- **Application startup file:** `passenger_wsgi.py` (or `app.py`)
- **Application Entry point:** `application`
- **Python version:** 3.13.11
### Test After Deployment:
1. https://columbiawindows.com/product-finder/test
2. https://columbiawindows.com/product-finder/login
3. Login with Master/Master
4. Test user management
## 🔧 Adding New Features
### Create a New Blueprint
1. Create `blueprints/your_feature.py`:
```python
from flask import Blueprint, render_template
from blueprints.auth import login_required, can_user
your_feature_bp = Blueprint('your_feature', __name__, url_prefix='/your-feature')
@your_feature_bp.route('/')
@login_required
def index():
return render_template('your_feature.html')
```
2. Register in `app.py`:
```python
from blueprints.your_feature import your_feature_bp
app.register_blueprint(your_feature_bp)
```
3. Create `templates/your_feature.html`
4. Test locally, then upload to server
## 📝 Benefits of Blueprint Structure
**Separation of Concerns:** Each feature in its own file
**Easy to Maintain:** Find and edit specific features quickly
**Scalable:** Add new features without touching existing code
**Testable:** Each blueprint can be tested independently
**Reusable:** Share decorators (login_required, permission_required) across blueprints
## 🛠️ Next Steps
- Add more blueprints for other features (quotes, products, etc.)
- Add more templates as needed
- Extend permission system
- Add location selection page
- Add the main product finder quiz
+87
View File
@@ -0,0 +1,87 @@
from flask import Flask, session, redirect, url_for
import secrets
import os
# Get the directory where this script is located
basedir = os.path.abspath(os.path.dirname(__file__))
# Initialize Flask with explicit paths
app = Flask(__name__,
template_folder=os.path.join(basedir, 'templates'),
static_folder=os.path.join(basedir, 'static'))
# Configure session - generate a random secret key
app.secret_key = secrets.token_hex(32)
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Import and register blueprints
from blueprints.auth import auth_bp
from blueprints.users import users_bp
app.register_blueprint(auth_bp)
app.register_blueprint(users_bp)
# Home route
@app.route('/')
def home():
"""Redirect to login if not authenticated, otherwise show home"""
if 'user_id' not in session:
return redirect(url_for('auth.login_page'))
# If user hasn't selected a location yet, redirect to selection
if not session.get('currentLocation') and len(session.get('accessibleLocations', [])) > 1:
return redirect(url_for('auth.select_location_page'))
return f"""
<html>
<head><title>Home</title></head>
<body style="font-family: Arial; padding: 20px;">
<h1>Welcome, {session.get('username', 'User')}!</h1>
<p>Current Location: {session.get('currentLocation', 'Not selected')}</p>
<ul>
<li><a href="{url_for('users.user_manager')}">Manage Users</a></li>
<li><a href="{url_for('auth.select_location_page')}">Change Location</a></li>
<li><a href="{url_for('auth.logout')}">Logout</a></li>
</ul>
</body>
</html>
"""
@app.route('/test')
def test():
"""Test route to verify app is running"""
import sys
return f"""
<html>
<head><title>Test Page</title></head>
<body style="font-family: Arial; padding: 20px;">
<h1>Flask App is Running!</h1>
<ul>
<li><strong>Python:</strong> {sys.version}</li>
<li><strong>Routes:</strong> {len(list(app.url_map.iter_rules()))}</li>
</ul>
<p><a href="{url_for('auth.login_page')}">Login</a></p>
</body>
</html>
"""
# Error handlers
@app.errorhandler(404)
def page_not_found(e):
return "<h1>404 - Page Not Found</h1>", 404
@app.errorhandler(500)
def internal_error(e):
return "<h1>500 - Internal Server Error</h1>", 500
# This is the critical line for Passenger
application = app
if __name__ == '__main__':
# Create necessary directories
os.makedirs(os.path.join(basedir, 'templates'), exist_ok=True)
os.makedirs(os.path.join(basedir, 'data'), exist_ok=True)
# Run the application
app.run(debug=True, host='0.0.0.0', port=8080)
+1
View File
@@ -0,0 +1 @@
# Blueprint package

Some files were not shown because too many files have changed in this diff Show More