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
+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"
}
}
}
}
]