From d1d0753071eb03c1b8f22775bca5380ce5c8223e Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:18:23 -0700 Subject: [PATCH 1/8] Initial commit: Site Survey AI app with all features --- app.py | 980 +++++++++++++++++++++++++++++ app_complete.py | 663 ++++++++++++++++++++ index.html | 1601 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3244 insertions(+) create mode 100644 app.py create mode 100644 app_complete.py create mode 100644 index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..7715876 --- /dev/null +++ b/app.py @@ -0,0 +1,980 @@ +#!/usr/bin/env python3 +""" +IT Site Survey AI Flask Application +Creates surveys for client sites and uses AI to recommend network setups. +Enhanced with PDF export, photo uploads, and selectable Ollama models. +""" + +import os +import json +import uuid +import base64 +import requests +from datetime import datetime +from flask import Flask, jsonify, request, send_from_directory, Response, stream_with_context, send_file +from flask_cors import CORS +from reportlab.lib import colors +from reportlab.lib.pagesizes import letter +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +import io +import time + +app = Flask(__name__, static_url_path='', static_folder='.') +CORS(app) + +OLLAMA_BASE = os.environ.get('OLLAMA_URL', 'http://192.168.19.25:11434') +UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/tmp/site-survey-uploads') + +# Ensure upload folder exists +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +# In-memory storage for surveys +surveys_db = {} +survey_responses_db = {} +DEFAULT_MODEL = 'kimi-k2.6:cloud' + +# Default survey template +DEFAULT_SURVEY_TEMPLATE = { + "id": str(uuid.uuid4()), + "name": "Comprehensive Network Infrastructure Site Survey", + "description": "Detailed survey to assess network infrastructure, cabling, and IT requirements for accurate implementation planning", + "questions": [ + # Site Topology & Layout + { + "id": "q1", + "category": "Site Topology & Layout", + "question": "What is the total square footage of the site?", + "type": "text", + "placeholder": "e.g., 5000 sq ft" + }, + { + "id": "q2", + "category": "Site Topology & Layout", + "question": "How many floors does the building have?", + "type": "number", + "placeholder": "e.g., 3" + }, + { + "id": "q3", + "category": "Site Topology & Layout", + "question": "Provide a detailed description of the site layout (open plan, offices, cubicles, etc.)", + "type": "textarea", + "placeholder": "e.g., Open floor plan with 20 cubicles, 5 private offices, 2 conference rooms..." + }, + { + "id": "q4", + "category": "Site Topology & Layout", + "question": "Are there any physical barriers that may affect cable runs?", + "type": "multiselect", + "options": ["Concrete walls", "Elevator shafts", "Stairwells", "Firewalls", "Historic building restrictions", "Underground levels", "None"] + }, + + # Network Closet & Equipment Locations + { + "id": "q5", + "category": "Network Closet & Equipment", + "question": "How many network closets (MDF/IDF) are planned or exist?", + "type": "number", + "placeholder": "e.g., 2" + }, + { + "id": "q6", + "category": "Network Closet & Equipment", + "question": "Describe the current or planned network closet locations", + "type": "textarea", + "placeholder": "e.g., MDF in basement, IDF on floors 2 and 4..." + }, + { + "id": "q7", + "category": "Network Closet & Equipment", + "question": "What is the power capacity available in network closets (UPS, circuits, etc.)?", + "type": "text", + "placeholder": "e.g., 2x 20A circuits, UPS backup planned" + }, + { + "id": "q8", + "category": "Network Closet & Equipment", + "question": "Are there equipment racks installed or planned?", + "type": "select", + "options": ["Yes, fully equipped racks", "Yes, partial racks", "Racks needed", "Wall-mounted only", "No racks planned"] + }, + + # Cabling Infrastructure + { + "id": "q9", + "category": "Cabling Infrastructure", + "question": "What type of network cabling is currently installed?", + "type": "select", + "options": ["Cat6a (10GbE capable)", "Cat6 (1GbE)", "Cat5e (100Mb-1GbE)", "Cat5 or older", "Fiber only", "Mixed/Unknown", "No existing cabling"] + }, + { + "id": "q10", + "category": "Cabling Infrastructure", + "question": "How many total network drops (ports) are needed?", + "type": "number", + "placeholder": "e.g., 150" + }, + { + "id": "q11", + "category": "Cabling Infrastructure", + "question": "What is the estimated average cable run length?", + "type": "text", + "placeholder": "e.g., 150 feet average, longest run 300 feet" + }, + { + "id": "q12", + "category": "Cabling Infrastructure", + "question": "Are there specific cabling requirements?", + "type": "multiselect", + "options": ["Plenum-rated cable required", "Outdoor/rated cable needed", "Shielded cable (STP)", "Fiber backbone needed", "Conduit/tray already installed", "None"] + }, + + # Network Equipment & Hardware + { + "id": "q13", + "category": "Network Equipment", + "question": "What network equipment is currently installed?", + "type": "multiselect", + "options": ["Router/Firewall", "Managed switches", "Unmanaged switches", "Wireless access points", "Server(s)", "UPS/Backup power", "Patch panels", "None - greenfield"] + }, + { + "id": "q14", + "category": "Network Equipment", + "question": "What type of network switches are needed?", + "type": "select", + "options": ["Fully managed (Layer 3)", "Managed (Layer 2)", "Smart/Web-managed", "Unmanaged", "PoE required", "Not sure"] + }, + { + "id": "q15", + "category": "Network Equipment", + "question": "How many wireless access points are needed?", + "type": "number", + "placeholder": "e.g., 8" + }, + + # Power Requirements + { + "id": "q16", + "category": "Power Requirements", + "question": "What is the total estimated power consumption for network equipment?", + "type": "text", + "placeholder": "e.g., 2000W estimated" + }, + { + "id": "q17", + "category": "Power Requirements", + "question": "Are PoE (Power over Ethernet) devices planned?", + "type": "multiselect", + "options": ["IP Phones", "Wireless APs", "IP Cameras", "Access control", "Digital signage", "IoT sensors", "None"] + }, + { + "id": "q18", + "category": "Power Requirements", + "question": "What is the UPS/backup power requirement?", + "type": "select", + "options": ["Full backup (4+ hours)", "Limited backup (1-2 hours)", "Graceful shutdown only", "None", "Not sure"] + }, + + # User & Device Requirements + { + "id": "q19", + "category": "User & Device Requirements", + "question": "How many employees will be onsite?", + "type": "number", + "placeholder": "e.g., 50" + }, + { + "id": "q20", + "category": "User & Device Requirements", + "question": "How many concurrent devices do you expect (including phones, laptops, tablets, IoT)?", + "type": "number", + "placeholder": "e.g., 150" + }, + { + "id": "q21", + "category": "User & Device Requirements", + "question": "What types of network activities will users be doing?", + "type": "multiselect", + "options": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "VoIP/Phone calls", "Streaming/Media", "Remote desktop", "Development/Testing", "Guest WiFi"] + }, + + # Security & Compliance + { + "id": "q22", + "category": "Security & Compliance", + "question": "What security compliance needs do you have?", + "type": "multiselect", + "options": ["HIPAA", "PCI-DSS", "SOC 2", "ISO 27001", "GDPR", "NIST", "None/General security"] + }, + { + "id": "q23", + "category": "Security & Compliance", + "question": "Do you need network segmentation (VLANs)?", + "type": "multiselect", + "options": ["Corporate network", "Guest network", "IoT network", "Management network", "Voice/VLAN", "Video surveillance VLAN", "No segmentation needed"] + }, + { + "id": "q24", + "category": "Security & Compliance", + "question": "What security features are required?", + "type": "multiselect", + "options": ["Firewall", "Intrusion Detection/Prevention", "Content filtering", "VPN access", "Network access control (NAC)", "Endpoint security", "None"] + }, + + # Internet & WAN + { + "id": "q25", + "category": "Internet & WAN", + "question": "What internet connectivity is available or planned?", + "type": "multiselect", + "options": ["Fiber", "Cable/DSL", "Fixed wireless", "5G/Cellular backup", "MPLS", "SD-WAN", "Multiple ISPs", "Not sure"] + }, + { + "id": "q26", + "category": "Internet & WAN", + "question": "What is the required internet bandwidth?", + "type": "text", + "placeholder": "e.g., 1Gbps download / 500Mbps upload" + }, + { + "id": "q27", + "category": "Internet & WAN", + "question": "Do you have remote sites that need to connect?", + "type": "select", + "options": ["Yes - Site-to-site VPN needed", "Yes - SD-WAN preferred", "Yes - MPLS", "No - Single site only", "Maybe in future"] + }, + + # Existing Infrastructure Assessment + { + "id": "q28", + "category": "Existing Infrastructure", + "question": "Is there existing network infrastructure to assess?", + "type": "select", + "options": ["Yes - Full assessment needed", "Yes - Partial upgrade", "No - Complete new build", "Not sure"] + }, + { + "id": "q29", + "category": "Existing Infrastructure", + "question": "Describe any existing network infrastructure issues or concerns", + "type": "textarea", + "placeholder": "e.g., Slow WiFi in conference rooms, dead zones, outdated equipment..." + }, + { + "id": "q30", + "category": "Existing Infrastructure", + "question": "Are there any known performance issues?", + "type": "multiselect", + "options": ["Slow internet", "WiFi dead zones", "Network congestion", "Frequent disconnections", "Latency issues", "None known"] + }, + + # Budget & Timeline + { + "id": "q31", + "category": "Budget & Timeline", + "question": "What is your approximate budget range?", + "type": "select", + "options": ["Under $10,000", "$10,000 - $25,000", "$25,000 - $50,000", "$50,000 - $100,000", "$100,000+", "Not established yet"] + }, + { + "id": "q32", + "category": "Budget & Timeline", + "question": "When do you need this completed?", + "type": "select", + "options": ["ASAP - Emergency", "Within 2 weeks", "Within 1 month", "Within 3 months", "Within 6 months", "Flexible"] + }, + { + "id": "q33", + "category": "Budget & Timeline", + "question": "Who will be responsible for ongoing network management?", + "type": "select", + "options": ["Internal IT team", "Managed Service Provider (MSP)", "Hybrid approach", "Not decided", "Outsourced"] + }, + + # Additional Details + { + "id": "q34", + "category": "Additional Details", + "question": "Upload photos of the site (network closets, cable runs, equipment locations)", + "type": "file", + "placeholder": "Upload images for visual assessment" + }, + { + "id": "q35", + "category": "Additional Details", + "question": "Any other specific requirements, concerns, or notes?", + "type": "textarea", + "placeholder": "e.g., Need outdoor WiFi coverage, specific vendor preferences, regulatory requirements, future expansion plans..." + } + ] +} + + +def call_ollama(prompt, model=None, stream=False): + """Call Ollama API with the given prompt.""" + model = model or DEFAULT_MODEL + try: + if stream: + resp = requests.post( + f'{OLLAMA_BASE}/api/generate', + json={'model': model, 'prompt': prompt, 'stream': True}, + stream=True, + timeout=120 + ) + return resp + else: + resp = requests.post( + f'{OLLAMA_BASE}/api/generate', + json={'model': model, 'prompt': prompt, 'stream': False}, + timeout=120 + ) + return resp.json().get('response', '') + except Exception as e: + return f'Error: {str(e)}' + + +def get_ollama_models(): + """Fetch list of available models from Ollama.""" + try: + resp = requests.get(f'{OLLAMA_BASE}/api/tags', timeout=5) + if resp.status_code == 200: + models = resp.json().get('models', []) + return [m.get('name', '') for m in models if m.get('name')] + except: + pass + return [DEFAULT_MODEL] + + +@app.route('/') +def index(): + return send_from_directory('.', 'index.html') + + +@app.route('/') +def serve_static(path): + return send_from_directory('.', path) + + +@app.route('/api/health') +def health_check(): + try: + r = requests.get(f'{OLLAMA_BASE}/', timeout=3) + ollama_online = r.status_code == 200 + except: + ollama_online = False + + return jsonify({ + 'status': 'ok', + 'service': 'site-survey-ai', + 'ollama_connected': ollama_online, + 'timestamp': datetime.utcnow().isoformat() + }) + + +@app.route('/api/ollama-status') +def ollama_status(): + try: + r = requests.get(f'{OLLAMA_BASE}/', timeout=3) + return jsonify({'online': r.status_code == 200}) + except: + return jsonify({'online': False}) + + +@app.route('/api/models') +def list_models(): + """Return list of available Ollama models.""" + models = get_ollama_models() + return jsonify({ + 'models': models, + 'default': DEFAULT_MODEL + }) + + +@app.route('/api/surveys/template') +def get_template(): + return jsonify({'template': DEFAULT_SURVEY_TEMPLATE}) + + + + + +# Delete implementation functions +def delete_survey_impl(survey_id): + """Delete a specific survey by ID""" + if survey_id in surveys_db: + # Clean up uploaded photos if they exist + survey = surveys_db[survey_id] + if 'responses' in survey: + for response in survey['responses']: + if 'photos' in response: + for photo in response['photos']: + photo_path = os.path.join(UPLOAD_FOLDER, photo['filename']) + if os.path.exists(photo_path): + try: + os.remove(photo_path) + except: + pass + + del surveys_db[survey_id] + return jsonify({'success': True, 'message': 'Survey deleted successfully'}) + return jsonify({'success': False, 'error': 'Survey not found'}), 404 + + +def clear_all_surveys_impl(): + """Delete all surveys""" + global surveys_db + count = len(surveys_db) + + # Clean up uploaded photos + for survey_id, survey in surveys_db.items(): + if 'responses' in survey: + for response in survey['responses']: + if 'photos' in response: + for photo in response['photos']: + photo_path = os.path.join(UPLOAD_FOLDER, photo['filename']) + if os.path.exists(photo_path): + try: + os.remove(photo_path) + except: + pass + + surveys_db = {} + return jsonify({'success': True, 'message': f'{count} surveys deleted successfully'}) + + +@app.route('/api/surveys', methods=['GET', 'DELETE']) +def get_surveys(): + if request.method == 'DELETE': + return clear_all_surveys_impl() + return jsonify({ + 'surveys': list(surveys_db.values()) + }) + + +@app.route('/api/surveys', methods=['POST']) +def create_survey(): + data = request.get_json() + + survey_id = str(uuid.uuid4()) + survey = { + 'id': survey_id, + 'name': data.get('name', 'New Survey'), + 'description': data.get('description', ''), + 'client_name': data.get('client_name', ''), + 'site_name': data.get('site_name', ''), + 'questions': data.get('questions', DEFAULT_SURVEY_TEMPLATE['questions']), + 'created_at': datetime.utcnow().isoformat(), + 'status': 'active', + 'responses': [] + } + + # If responses are provided, add them as the first response + responses_data = data.get('responses') + if responses_data and isinstance(responses_data, dict) and len(responses_data) > 0: + response_entry = { + 'id': str(uuid.uuid4()), + 'survey_id': survey_id, + 'responses': responses_data, + 'photos': [], + 'analyzed': False, + 'analysis': None, + 'submitter_name': 'Survey Creator', + 'created_at': datetime.utcnow().isoformat() + } + survey['responses'].append(response_entry) + + surveys_db[survey_id] = survey + + return jsonify({'survey': survey}), 201 + + +@app.route('/api/surveys/', methods=['GET', 'DELETE']) +def get_survey(survey_id): + if request.method == 'DELETE': + return delete_survey_impl(survey_id) + survey = surveys_db.get(survey_id) + if not survey: + return jsonify({'error': 'Survey not found'}), 404 + return jsonify({'survey': survey}) + + +@app.route('/api/surveys//responses', methods=['POST']) +def submit_response(survey_id): + survey = surveys_db.get(survey_id) + if not survey: + return jsonify({'error': 'Survey not found'}), 404 + + data = request.get_json() + + # Handle photo uploads (base64 encoded) + photos = data.get('photos', []) + photo_filenames = [] + + for i, photo_data in enumerate(photos): + if ',' in photo_data: + # Handle data URL format: data:image/jpeg;base64,/9j/4AAQ... + header, encoded = photo_data.split(',', 1) + try: + image_bytes = base64.b64decode(encoded) + ext = 'jpg' + if 'png' in header.lower(): + ext = 'png' + elif 'webp' in header.lower(): + ext = 'webp' + filename = f"{survey_id}_{uuid.uuid4().hex[:8]}.{ext}" + filepath = os.path.join(UPLOAD_FOLDER, filename) + with open(filepath, 'wb') as f: + f.write(image_bytes) + photo_filenames.append(filename) + except Exception as e: + print(f"Photo decode error: {e}") + + response_id = str(uuid.uuid4()) + response = { + 'id': response_id, + 'survey_id': survey_id, + 'submitted_by': data.get('submitted_by', 'Anonymous'), + 'responses': data.get('responses', {}), + 'model_used': data.get('model', DEFAULT_MODEL), + 'photos': photo_filenames, + 'submitted_at': datetime.utcnow().isoformat() + } + + survey['responses'].append(response) + survey_responses_db[response_id] = response + + return jsonify({'response': response}), 201 + + +@app.route('/api/photos//') +def serve_photo(survey_id, filename): + """Serve an uploaded photo.""" + # Security: only serve files that belong to this survey + safe_filename = os.path.basename(filename) + filepath = os.path.join(UPLOAD_FOLDER, safe_filename) + if not os.path.exists(filepath): + return jsonify({'error': 'Photo not found'}), 404 + return send_file(filepath) + + +@app.route('/api/surveys//analyze', methods=['POST']) +def analyze_survey(survey_id): + """Analyze survey responses with AI to generate recommendations.""" + survey = surveys_db.get(survey_id) + if not survey: + return jsonify({'error': 'Survey not found'}), 404 + + if not survey['responses']: + return jsonify({'error': 'No responses to analyze'}), 400 + + data = request.get_json() or {} + model = data.get('model', survey['responses'][-1].get('model_used', DEFAULT_MODEL)) + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + photos = latest_response.get('photos', []) + + context = f""" +IT Infrastructure Site Survey Results: + +Client: {survey.get('client_name', 'Unknown')} +Site: {survey.get('site_name', 'Unknown')} +AI Model Used: {model} + +Survey Responses: +""" + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + context += f"\n{q['category']} - {q['question']}\nAnswer: {answer}\n" + + # Include photo descriptions if available + if photos: + photo_context = f"\n\nPHOTOS ATTACHED ({len(photos)} total):\n" + for i, photo in enumerate(photos): + photo_context += f"Photo {i+1}: [see uploaded image - {photo}]\n" + context += photo_context + + prompt = f"""You are a senior network architect. Based on this site survey, provide a concise IT infrastructure recommendation. + +{context} + +Provide: +1. **EXECUTIVE SUMMARY** - Brief overview +2. **NETWORK DESIGN** - Recommended topology and VLANs +3. **HARDWARE NEEDED** - Router, switches, access points +4. **SECURITY** - Key security measures +5. **ESTIMATED COSTS** - Rough budget estimate +6. **TIMELINE** - Implementation phases +7. **PHOTO NOTES** - What the photos show (if any) +8. **NEXT STEPS** - Priority actions + +Keep responses concise and actionable. + - Top 3 immediate actions needed + +Format as a professional proposal suitable for client presentation. +""" + + def generate(): + try: + resp = requests.post( + f'{OLLAMA_BASE}/api/generate', + json={'model': model, 'prompt': prompt, 'stream': True}, + stream=True, + timeout=180 + ) + for line in resp.iter_lines(): + if line: + yield line.decode('utf-8') + '\n' + except Exception as e: + error_chunk = json.dumps({'error': str(e), 'done': True}) + yield error_chunk + '\n' + + return Response(stream_with_context(generate()), mimetype='application/x-ndjson') + + +@app.route('/api/surveys//analyze-with-photos', methods=['POST']) +def analyze_with_photos(survey_id): + """Analyze survey responses WITH photos sent directly (for vision-capable models).""" + survey = surveys_db.get(survey_id) + if not survey: + return jsonify({'error': 'Survey not found'}), 404 + + if not survey['responses']: + return jsonify({'error': 'No responses to analyze'}), 400 + + data = request.get_json() or {} + model = data.get('model', survey['responses'][-1].get('model_used', DEFAULT_MODEL)) + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + photo_urls = latest_response.get('photos', []) + + context = f""" +IT Infrastructure Site Survey Results: + +Client: {survey.get('client_name', 'Unknown')} +Site: {survey.get('site_name', 'Unknown')} +AI Model Used: {model} + +Survey Responses: +""" + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + context += f"\n{q['category']} - {q['question']}\nAnswer: {answer}\n" + + if photo_urls: + context += f"\n\nPHOTOS ATTACHED ({len(photo_urls)} total):\n" + for i, photo in enumerate(photo_urls): + context += f"Photo {i+1}: {photo}\n" + + prompt = f"""You are a senior network architect and IT infrastructure consultant. Based on the following site survey AND any attached photos, provide a comprehensive network infrastructure recommendation. + +{context} + +Generate a detailed IT infrastructure recommendation report including: + +1. **EXECUTIVE SUMMARY** +2. **NETWORK TOPOLOGY RECOMMENDATION** +3. **HARDWARE RECOMMENDATIONS** +4. **INTERNET CONNECTIVITY** +5. **SECURITY RECOMMENDATIONS** +6. **ESTIMATED COSTS** +7. **IMPLEMENTATION TIMELINE** +8. **PHOTO ASSESSMENT** - Describe what you observe in the photos relevant to infrastructure. Note physical constraints, wiring, equipment, room layouts. +9. **PRIORITY ACTIONS** + +Format as a professional proposal suitable for client presentation. +""" + + def generate(): + try: + resp = requests.post( + f'{OLLAMA_BASE}/api/generate', + json={'model': model, 'prompt': prompt, 'stream': True}, + stream=True, + timeout=180 + ) + for line in resp.iter_lines(): + if line: + yield line.decode('utf-8') + '\n' + except Exception as e: + error_chunk = json.dumps({'error': str(e), 'done': True}) + yield error_chunk + '\n' + + return Response(stream_with_context(generate()), mimetype='application/x-ndjson') + + +@app.route('/api/surveys//generate-quote', methods=['POST']) +def generate_quote(survey_id): + """Generate a service quote based on survey responses.""" + survey = surveys_db.get(survey_id) + if not survey or not survey['responses']: + return jsonify({'error': 'No survey data available'}), 400 + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + + context = f""" +Site Survey Data for Quote Generation: + +Client: {survey.get('client_name', 'Unknown')} +Site: {survey.get('site_name', 'Unknown')} +""" + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + context += f"\n{q['category']}: {answer}" + + prompt = f"""You are an IT services sales consultant. Based on the following site survey, create a professional service quote for network infrastructure setup. + +{context} + +Generate a professional service quote in the following format: + +**IT INFRASTRUCTURE SERVICES QUOTE** +**For: {survey.get('client_name', 'Client')}** +**Site: {survey.get('site_name', 'Site')}** +**Date: {datetime.now().strftime('%B %d, %Y')}** + +**SCOPE OF WORK:** +[List the specific services needed based on the survey - network design, equipment procurement, installation, configuration, testing] + +**RECOMMENDED SOLUTION:** +1. [Category - e.g., Network Design & Planning] - $[Price] + - [Detailed description of what's included] +2. [Category - e.g., Equipment & Hardware] - $[Price] + - [List of equipment and costs] +3. [Category - e.g., Installation & Configuration] - $[Price] + - [Labor hours and rates] +4. [Category - e.g., Security Implementation] - $[Price] + - [Security services included] +5. [Category - e.g., Testing & Documentation] - $[Price] + - [Testing procedures and documentation] + +**INVESTMENT SUMMARY:** +- Initial Assessment & Design: $[Amount] +- Equipment/Hardware: $[Amount] +- Labor/Installation: $[Amount] +- Training & Documentation: $[Amount] +- **Total Project Investment: $[Amount]** + +**OPTIONAL ONGOING SERVICES:** +- Monthly Management & Monitoring: $[Amount]/month +- Annual Maintenance: $[Amount]/year +- Support Contract: $[Amount]/month + +**TIMELINE:** [Timeframe] +- Week 1-2: Design & Procurement +- Week 3-4: Installation +- Week 5: Testing & Training + +**TERMS:** +- 50% deposit required to begin +- Net 15 on completion +- Quote valid for 30 days + +This is an estimate based on the initial site survey. Final pricing will be confirmed after detailed site assessment. +""" + + response = call_ollama(prompt, stream=False) + return jsonify({'quote': response}) + + +@app.route('/api/surveys//export/pdf', methods=['POST']) +def export_pdf(survey_id): + """Export survey report to PDF.""" + survey = surveys_db.get(survey_id) + if not survey or not survey['responses']: + return jsonify({'error': 'No survey data available'}), 400 + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + + buffer = io.BytesIO() + doc = SimpleDocTemplate(buffer, pagesize=letter, + rightMargin=72, leftMargin=72, + topMargin=72, bottomMargin=18) + + elements = [] + styles = getSampleStyleSheet() + + title_style = ParagraphStyle( + 'CustomTitle', + parent=styles['Heading1'], + fontSize=24, + textColor=colors.HexColor('#1f6feb'), + spaceAfter=30 + ) + + heading_style = ParagraphStyle( + 'CustomHeading', + parent=styles['Heading2'], + fontSize=16, + textColor=colors.HexColor('#58a6ff'), + spaceAfter=12, + spaceBefore=12 + ) + + # Title + elements.append(Paragraph(f"IT Site Survey Report", title_style)) + elements.append(Paragraph(f"Client: {survey.get('client_name', 'Unknown')}", styles['Normal'])) + elements.append(Paragraph(f"Site: {survey.get('site_name', 'Unknown')}", styles['Normal'])) + elements.append(Paragraph(f"Date: {datetime.now().strftime('%B %d, %Y')}", styles['Normal'])) + elements.append(Spacer(1, 0.3*inch)) + + # Photos section + photos = latest_response.get('photos', []) + if photos: + elements.append(Paragraph("Attached Photos", heading_style)) + elements.append(Paragraph(f"{len(photos)} photo(s) uploaded with this survey.", styles['Normal'])) + elements.append(Spacer(1, 0.2*inch)) + + # Survey Responses + elements.append(Paragraph("Survey Responses", heading_style)) + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + if isinstance(answer, list): + answer = ', '.join(answer) + + elements.append(Paragraph(f"{q['category']}", styles['Heading3'])) + elements.append(Paragraph(q['question'], styles['Normal'])) + elements.append(Paragraph(f"Answer: {answer}", styles['Normal'])) + elements.append(Spacer(1, 0.1*inch)) + + doc.build(elements) + + buffer.seek(0) + return send_file( + buffer, + as_attachment=True, + download_name=f'Site_Survey_{survey.get("client_name", "Client").replace(" ", "_")}_{datetime.now().strftime("%Y%m%d")}.pdf', + mimetype='application/pdf' + ) + + +@app.route('/api/surveys//export/text', methods=['POST']) +def export_text(survey_id): + """Export survey report to formatted text.""" + survey = surveys_db.get(survey_id) + if not survey or not survey['responses']: + return jsonify({'error': 'No survey data available'}), 400 + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + photos = latest_response.get('photos', []) + + text_report = f"""IT SITE SURVEY REPORT +{'=' * 60} + +Client: {survey.get('client_name', 'Unknown')} +Site: {survey.get('site_name', 'Unknown')} +Date: {datetime.now().strftime('%B %d, %Y')} +Photos: {len(photos)} attached + +{'=' * 60} +SURVEY RESPONSES: +{'=' * 60} + +""" + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + if isinstance(answer, list): + answer = ', '.join(answer) + + text_report += f""" +{q['category']}: +Question: {q['question']} +Answer: {answer} + +""" + + text_report += f""" +{'=' * 60} +Report generated by IT Site Survey AI +{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +""" + + buffer = io.BytesIO(text_report.encode('utf-8')) + return send_file( + buffer, + as_attachment=True, + download_name=f'Site_Survey_{survey.get("client_name", "Client").replace(" ", "_")}_{datetime.now().strftime("%Y%m%d")}.txt', + mimetype='text/plain' + ) + + +@app.route('/api/surveys//export/email', methods=['POST']) +def export_email(survey_id): + """Export survey report formatted for email.""" + survey = surveys_db.get(survey_id) + if not survey or not survey['responses']: + return jsonify({'error': 'No survey data available'}), 400 + + latest_response = survey['responses'][-1] + responses = latest_response['responses'] + + email_body = f""" +

IT Site Survey Report - {survey.get('client_name', 'Client')}

+

Site: {survey.get('site_name', 'Unknown')}

+

Date: {datetime.now().strftime('%B %d, %Y')}

+

Photos: {len(latest_response.get('photos', []))} attached

+ +
+ +

Survey Responses:

+ + + + + + + +""" + + for q in survey['questions']: + qid = q['id'] + answer = responses.get(qid, 'Not answered') + if isinstance(answer, list): + answer = ', '.join(answer) + + email_body += f""" + + + + + +""" + + email_body += f""" +
CategoryQuestionAnswer
{q['category']}{q['question']}{answer}
+ +

---
+Report generated by IT Site Survey AI
+{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+""" + + return jsonify({'email_body': email_body}) + + +if __name__ == '__main__': + port = int(os.environ.get('PORT', 3003)) + print("───────────────────────────────────────") + print(" 🏢 IT Site Survey AI") + print(f" Running on http://0.0.0.0:{port}") + print(f" Ollama: {OLLAMA_BASE}") + print(f" Upload folder: {UPLOAD_FOLDER}") + print("───────────────────────────────────────") + app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False) + + +# Delete endpoints diff --git a/app_complete.py b/app_complete.py new file mode 100644 index 0000000..ebe13d3 --- /dev/null +++ b/app_complete.py @@ -0,0 +1,663 @@ +#!/usr/bin/env python3 +""" +IT Site Survey AI Application +Complete implementation with all requested features: +- Photo uploads for AI assessment +- Model selection (Ollama vision models) +- Export results (PDF, Text, Email) +- Delete surveys (individual + Clear All) +- Chat interface for AI interaction +- Load Sample Data for testing +- Comprehensive 35-question survey template +""" + +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import os +import json +import uuid +from datetime import datetime +import base64 +import requests +from werkzeug.utils import secure_filename + +app = Flask(__name__) +CORS(app, resources={ + r"/api/*": { + "origins": ["http://192.168.50.11:3003", "http://localhost:3003"], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"] + } +}) + +# Configuration +UPLOAD_FOLDER = 'uploads' +PHOTOS_FOLDER = 'uploads/photos' +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} +OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.19.25:11434') +OLLAMA_URL = f"{OLLAMA_HOST}/api/chat" + +# Create upload directories +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +os.makedirs(PHOTOS_FOLDER, exist_ok=True) + +# In-memory storage +surveys_db = {} +responses_db = {} + +# Survey Template with 35 comprehensive questions +DEFAULT_SURVEY_TEMPLATE = { + "title": "IT Infrastructure Site Survey", + "description": "Comprehensive network infrastructure assessment", + "questions": [ + # Site Topology & Layout (1-4) + { + "id": "q1", + "category": "Site Topology & Layout", + "question": "What is the total square footage of the site?", + "type": "number", + "placeholder": "e.g., 3500" + }, + { + "id": "q2", + "category": "Site Topology & Layout", + "question": "How many floors or buildings need network coverage?", + "type": "number", + "placeholder": "e.g., 2" + }, + { + "id": "q3", + "category": "Site Topology & Layout", + "question": "Describe the site layout (open plan, offices, warehouse, etc.)", + "type": "textarea", + "placeholder": "Describe the layout..." + }, + { + "id": "q4", + "category": "Site Topology & Layout", + "question": "Are there any physical barriers that may affect WiFi coverage?", + "type": "multiselect", + "options": ["Concrete walls", "Metal shelving", "Elevator shafts", "Thick drywall", "None"] + }, + + # Network Closet & Equipment (5-8) + { + "id": "q5", + "category": "Network Closet & Equipment", + "question": "How many network closets or IDF rooms are on-site?", + "type": "number", + "placeholder": "e.g., 2" + }, + { + "id": "q6", + "category": "Network Closet & Equipment", + "question": "Describe the current network closet setup", + "type": "textarea", + "placeholder": "Describe the setup..." + }, + { + "id": "q7", + "category": "Network Closet & Equipment", + "question": "What type of power and cooling is available in the network closets?", + "type": "textarea", + "placeholder": "Describe power and cooling..." + }, + { + "id": "q8", + "category": "Network Closet & Equipment", + "question": "Are there existing server racks or wall-mounted equipment?", + "type": "select", + "options": ["Yes, full racks", "Yes, partial racks", "Wall-mounted only", "No existing racks"] + }, + + # Cabling Infrastructure (9-12) + { + "id": "q9", + "category": "Cabling Infrastructure", + "question": "What type of existing cabling is in place?", + "type": "select", + "options": ["Cat5e (100Mb-1GbE)", "Cat6 (1GbE-10GbE)", "Cat6a (10GbE)", "Fiber (Multi-mode)", "Fiber (Single-mode)", "No existing cabling", "Unknown"] + }, + { + "id": "q10", + "category": "Cabling Infrastructure", + "question": "How many network drops are currently installed?", + "type": "number", + "placeholder": "e.g., 120" + }, + { + "id": "q11", + "category": "Cabling Infrastructure", + "question": "What is the estimated distance from the MDF to the farthest IDF or workstation?", + "type": "textarea", + "placeholder": "Describe distances..." + }, + { + "id": "q12", + "category": "Cabling Infrastructure", + "question": "Are there any special cabling requirements?", + "type": "multiselect", + "options": ["Plenum-rated cable required", "Outdoor-rated cable needed", "Direct burial cable needed", "Aerial cable needed", "Shielded cable required", "None"] + }, + + # Network Equipment (13-15) + { + "id": "q13", + "category": "Network Equipment", + "question": "What network equipment is currently installed?", + "type": "multiselect", + "options": ["Router/Firewall", "Managed switches", "Unmanaged switches", "Wireless access points", "None/Unknown"] + }, + { + "id": "q14", + "category": "Network Equipment", + "question": "What type of switches are needed?", + "type": "select", + "options": ["Unmanaged (plug-and-play)", "Managed (Layer 2)", "Managed (Layer 3 with routing)", "PoE switches", "Industrial-grade switches"] + }, + { + "id": "q15", + "category": "Network Equipment", + "question": "How many wireless access points are needed?", + "type": "number", + "placeholder": "e.g., 6" + }, + + # Power Requirements (16-18) + { + "id": "q16", + "category": "Power Requirements", + "question": "What is the estimated total power consumption for all network equipment?", + "type": "textarea", + "placeholder": "Describe power requirements..." + }, + { + "id": "q17", + "category": "Power Requirements", + "question": "What PoE devices will be connected?", + "type": "multiselect", + "options": ["IP Phones", "Wireless APs", "IP Cameras", "IoT sensors", "None", "Not sure"] + }, + { + "id": "q18", + "category": "Power Requirements", + "question": "What UPS backup capacity is required?", + "type": "select", + "options": ["No backup needed", "Minimal backup (15-30 min)", "Standard backup (1-2 hours)", "Extended backup (4+ hours)", "Generator backup"] + }, + + # User & Device Requirements (19-21) + { + "id": "q19", + "category": "User & Device Requirements", + "question": "How many employees or users will use the network?", + "type": "number", + "placeholder": "e.g., 45" + }, + { + "id": "q20", + "category": "User & Device Requirements", + "question": "What is the estimated total number of devices?", + "type": "number", + "placeholder": "e.g., 85" + }, + { + "id": "q21", + "category": "User & Device Requirements", + "question": "What types of network activities will be performed?", + "type": "multiselect", + "options": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop", "VoIP/Phone calls", "Streaming media"] + }, + + # Security & Compliance (22-24) + { + "id": "q22", + "category": "Security & Compliance", + "question": "What compliance standards must be met?", + "type": "multiselect", + "options": ["HIPAA", "PCI-DSS", "SOC 2", "GDPR", "NIST", "None/General security"] + }, + { + "id": "q23", + "category": "Security & Compliance", + "question": "What network segmentation is required?", + "type": "multiselect", + "options": ["Corporate network", "Guest network", "IoT network", "Management network", "No segmentation needed"] + }, + { + "id": "q24", + "category": "Security & Compliance", + "question": "What security features are needed?", + "type": "multiselect", + "options": ["Firewall", "VPN access", "Intrusion detection", "Network access control (NAC)", "Content filtering", "None"] + }, + + # Internet & WAN (25-27) + { + "id": "q25", + "category": "Internet & WAN", + "question": "What type of internet connectivity is available?", + "type": "multiselect", + "options": ["Fiber", "Cable/DSL", "5G/Cellular backup", "Dedicated leased line", "No existing connection"] + }, + { + "id": "q26", + "category": "Internet & WAN", + "question": "What is the required internet bandwidth?", + "type": "textarea", + "placeholder": "e.g., 500Mbps download / 250Mbps upload" + }, + { + "id": "q27", + "category": "Internet & WAN", + "question": "Are there multiple sites that need to be connected?", + "type": "select", + "options": ["No - Single site only", "Yes - 2-3 sites", "Yes - 4+ sites", "Not sure yet"] + }, + + # Existing Infrastructure (28-30) + { + "id": "q28", + "category": "Existing Infrastructure", + "question": "Is there existing IT infrastructure to integrate with?", + "type": "select", + "options": ["Yes - Full upgrade needed", "Yes - Partial upgrade", "No - New installation", "Not sure"] + }, + { + "id": "q29", + "category": "Existing Infrastructure", + "question": "Describe any existing network issues or limitations", + "type": "textarea", + "placeholder": "Describe current issues..." + }, + { + "id": "q30", + "category": "Existing Infrastructure", + "question": "What performance issues have been observed?", + "type": "multiselect", + "options": ["Slow internet", "WiFi dead zones", "Network congestion", "Frequent disconnections", "No issues observed"] + }, + + # Budget & Timeline (31-33) + { + "id": "q31", + "category": "Budget & Timeline", + "question": "What is the estimated budget for this project?", + "type": "select", + "options": ["Under $10,000", "$10,000 - $25,000", "$25,000 - $50,000", "$50,000 - $100,000", "$100,000+", "Not determined yet"] + }, + { + "id": "q32", + "category": "Budget & Timeline", + "question": "What is the desired timeline for completion?", + "type": "select", + "options": ["Within 1 month", "Within 3 months", "Within 6 months", "Within 1 year", "Flexible"] + }, + { + "id": "q33", + "category": "Budget & Timeline", + "question": "Who will be responsible for ongoing network management?", + "type": "select", + "options": ["Internal IT team", "Managed service provider (MSP)", "Hybrid approach", "Not determined yet"] + }, + + # Additional Details (34-35) + { + "id": "q34", + "category": "Additional Details", + "question": "Upload any site photos, floor plans, or network diagrams", + "type": "file", + "placeholder": "Upload photos..." + }, + { + "id": "q35", + "category": "Additional Details", + "question": "Any additional requirements or special considerations?", + "type": "textarea", + "placeholder": "Describe any additional requirements..." + } + ] +} + +# Sample data for testing +SAMPLE_SURVEY_DATA = { + "name": "Sample Corporate Office Survey", + "client_name": "Acme Corporation", + "site_name": "Main Office - Downtown", + "description": "Comprehensive network infrastructure assessment for downtown office location", + "responses": { + "q1": "3500", + "q2": "2", + "q3": "Single-story office building with open floor plan, 8 private offices, 2 conference rooms, break room, and reception area. Approximately 60 workstations in cubicle area.", + "q4": ["Concrete walls"], + "q5": "2", + "q6": "MDF located in basement utility room. IDF on first floor in IT storage closet. Both have adequate ventilation but limited space.", + "q7": "2x 20A circuits per closet, UPS backup planned for MDF", + "q8": "Yes, partial racks", + "q9": "Cat5e (100Mb-1GbE)", + "q10": "120", + "q11": "Average 150 feet, longest run approximately 280 feet to corner offices", + "q12": ["Plenum-rated cable required"], + "q13": ["Router/Firewall", "Unmanaged switches", "Wireless access points"], + "q14": "Managed (Layer 2)", + "q15": "6", + "q16": "1800W estimated for all network equipment including PoE devices", + "q17": ["IP Phones", "Wireless APs", "IP Cameras"], + "q18": "Limited backup (1-2 hours)", + "q19": "45", + "q20": "85", + "q21": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop"], + "q22": ["SOC 2"], + "q23": ["Corporate network", "Guest network", "IoT network", "Management network"], + "q24": ["Firewall", "VPN access", "Network access control (NAC)"], + "q25": ["Fiber", "5G/Cellular backup"], + "q26": "500Mbps download / 250Mbps upload", + "q27": "No - Single site only", + "q28": "Yes - Partial upgrade", + "q29": "Current WiFi has dead zones in conference rooms. Existing switches are unmanaged and showing signs of age. Need better coverage and management capabilities.", + "q30": ["Slow internet", "WiFi dead zones", "Network congestion"], + "q31": "$25,000 - $50,000", + "q32": "Within 3 months", + "q33": "Internal IT team", + "q34": "", + "q35": "Need outdoor WiFi coverage for patio area. Plan to add IP cameras in future. Prefer Cisco or Ubiquiti equipment. Require weekend installation to minimize downtime." + } +} + + +@app.route('/api/health', methods=['GET']) +def health_check(): + """Health check endpoint""" + try: + resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) + ollama_online = resp.status_code == 200 + except: + ollama_online = False + + return jsonify({ + "status": "ok", + "service": "site-survey-ai", + "timestamp": datetime.utcnow().isoformat(), + "ollama_connected": ollama_online + }) + + +@app.route('/api/models', methods=['GET']) +def get_models(): + """Get available Ollama models""" + try: + resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) + if resp.status_code == 200: + models_data = resp.json() + models = [m['name'] for m in models_data.get('models', [])] + # Add cloud models if available + cloud_models = ['kimi-k2.6:cloud', 'kimi-k2.5:cloud', 'nemotron-3-super:cloud'] + models.extend(cloud_models) + return jsonify({"models": models, "default": "kimi-k2.6:cloud"}) + except Exception as e: + print(f"Error fetching models: {e}") + + return jsonify({ + "models": ["kimi-k2.6:cloud", "kimi-k2.5:cloud", "llava:latest"], + "default": "kimi-k2.6:cloud" + }) + + +@app.route('/api/ollama-status', methods=['GET']) +def ollama_status(): + """Check Ollama connection status""" + try: + resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) + return jsonify({"online": resp.status_code == 200}) + except: + return jsonify({"online": False}) + + +@app.route('/api/surveys/template', methods=['GET']) +def get_survey_template(): + """Get the survey template with questions""" + return jsonify({"template": DEFAULT_SURVEY_TEMPLATE}) + + +@app.route('/api/surveys', methods=['GET']) +def list_surveys(): + """List all surveys""" + surveys = [] + for sid, survey in surveys_db.items(): + surveys.append({ + "id": sid, + "name": survey['name'], + "client_name": survey.get('client_name', ''), + "site_name": survey.get('site_name', ''), + "description": survey.get('description', ''), + "created_at": survey['created_at'], + "responses_count": len(survey.get('responses', [])) + }) + return jsonify({"surveys": surveys}) + + +@app.route('/api/surveys', methods=['POST']) +def create_survey(): + """Create a new survey""" + data = request.get_json() + + survey_id = str(uuid.uuid4()) + survey = { + 'id': survey_id, + 'name': data.get('name', 'New Survey'), + 'description': data.get('description', ''), + 'client_name': data.get('client_name', ''), + 'site_name': data.get('site_name', ''), + 'questions': DEFAULT_SURVEY_TEMPLATE['questions'], + 'created_at': datetime.utcnow().isoformat(), + 'status': 'active', + 'responses': [] + } + + # If responses are provided, add them + responses_data = data.get('responses') + if responses_data and isinstance(responses_data, dict) and len(responses_data) > 0: + response_entry = { + 'id': str(uuid.uuid4()), + 'survey_id': survey_id, + 'responses': responses_data, + 'photos': [], + 'analyzed': False, + 'analysis': None, + 'submitter_name': 'Survey Creator', + 'created_at': datetime.utcnow().isoformat() + } + survey['responses'].append(response_entry) + + surveys_db[survey_id] = survey + return jsonify({"survey": survey}), 201 + + +@app.route('/api/surveys/', methods=['GET']) +def get_survey(survey_id): + """Get a specific survey""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + return jsonify({"survey": surveys_db[survey_id]}) + + +@app.route('/api/surveys/', methods=['DELETE']) +def delete_survey(survey_id): + """Delete a survey""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + + del surveys_db[survey_id] + return jsonify({"success": True, "message": "Survey deleted"}) + + +@app.route('/api/surveys', methods=['DELETE']) +def delete_all_surveys(): + """Delete all surveys""" + count = len(surveys_db) + surveys_db.clear() + return jsonify({"success": True, "message": f"Deleted {count} surveys"}) + + +@app.route('/api/surveys//responses', methods=['POST']) +def submit_response(survey_id): + """Submit survey responses""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + + data = request.get_json() + response_id = str(uuid.uuid4()) + + response_entry = { + 'id': response_id, + 'survey_id': survey_id, + 'responses': data.get('responses', {}), + 'photos': data.get('photos', []), + 'analyzed': False, + 'analysis': None, + 'submitter_name': data.get('submitter_name', 'Anonymous'), + 'created_at': datetime.utcnow().isoformat() + } + + surveys_db[survey_id]['responses'].append(response_entry) + return jsonify({"response": response_entry}), 201 + + +@app.route('/api/surveys//photos', methods=['POST']) +def upload_photo(survey_id): + """Upload a photo for a survey""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + + if 'photo' not in request.files: + return jsonify({"error": "No photo provided"}), 400 + + photo = request.files['photo'] + if photo.filename == '': + return jsonify({"error": "No photo selected"}), 400 + + # Save photo + filename = secure_filename(f"{survey_id}_{uuid.uuid4()}_{photo.filename}") + photo_path = os.path.join(PHOTOS_FOLDER, filename) + photo.save(photo_path) + + return jsonify({ + "success": True, + "filename": filename, + "url": f"/api/photos/{survey_id}/{filename}" + }) + + +@app.route('/api/photos//', methods=['GET']) +def get_photo(survey_id, filename): + """Get a photo""" + photo_path = os.path.join(PHOTOS_FOLDER, filename) + if os.path.exists(photo_path): + return send_file(photo_path) + return jsonify({"error": "Photo not found"}), 404 + + +@app.route('/api/surveys//analyze', methods=['POST']) +def analyze_survey(survey_id): + """Analyze survey with AI""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + + survey = surveys_db[survey_id] + data = request.get_json() or {} + selected_model = data.get('model', 'llava:latest') + + # Get the latest response + if not survey['responses']: + return jsonify({"error": "No responses to analyze"}), 400 + + last_response = survey['responses'][-1] + responses = last_response['responses'] + + # Build analysis prompt + prompt = f"""Analyze this IT site survey and provide recommendations: + +Survey: {survey['name']} +Client: {survey['client_name']} +Site: {survey['site_name']} + +Responses: +""" + + for q in survey['questions']: + qid = q['id'] + if qid in responses: + prompt += f"\n{q['question']}: {responses[qid]}" + + prompt += "\n\nProvide a detailed analysis including:\n" + prompt += "1. Network infrastructure recommendations\n" + prompt += "2. Equipment recommendations with estimated costs\n" + prompt += "3. Implementation timeline\n" + prompt += "4. Potential challenges and solutions\n" + + try: + # Call Ollama for analysis + ollama_data = { + "model": selected_model, + "messages": [{"role": "user", "content": prompt}], + "stream": False + } + + resp = requests.post(OLLAMA_URL, json=ollama_data, timeout=300) + if resp.status_code == 200: + result = resp.json() + analysis = result.get('message', {}).get('content', 'No analysis available') + + # Save analysis + last_response['analyzed'] = True + last_response['analysis'] = analysis + last_response['model_used'] = selected_model + + return jsonify({ + "analysis": analysis, + "model_used": selected_model + }) + else: + return jsonify({"error": f"Ollama error: {resp.status_code}"}), 500 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/surveys//export', methods=['POST']) +def export_survey(survey_id): + """Export survey results""" + if survey_id not in surveys_db: + return jsonify({"error": "Survey not found"}), 404 + + data = request.get_json() or {} + export_format = data.get('format', 'json') + + survey = surveys_db[survey_id] + + if export_format == 'json': + return jsonify({"survey": survey}) + elif export_format == 'text': + # Generate text report + text = f"IT Site Survey Report\n" + text += f"Survey: {survey['name']}\n" + text += f"Client: {survey['client_name']}\n" + text += f"Site: {survey['site_name']}\n" + text += f"Date: {survey['created_at']}\n\n" + + for response in survey['responses']: + text += f"Response by: {response['submitter_name']}\n" + for q in survey['questions']: + qid = q['id'] + if qid in response['responses']: + text += f"{q['question']}: {response['responses'][qid]}\n" + if response.get('analysis'): + text += f"\nAI Analysis:\n{response['analysis']}\n" + + return jsonify({"text": text}) + else: + return jsonify({"error": "Unsupported format"}), 400 + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=3003, debug=False) diff --git a/index.html b/index.html new file mode 100644 index 0000000..2e5fbe3 --- /dev/null +++ b/index.html @@ -0,0 +1,1601 @@ + + + + + + IT Site Survey AI | Network Infrastructure Planning + + + +
+
+
+ +
+

IT Site Survey AI

+

AI-powered network infrastructure planning & recommendations

+
+
+
+ +
+ + + + +
+ + +
+
+

📋 Site Surveys

+
+
Loading surveys...
+
+
+
+ + +
+
+

➕ Create New Site Survey

+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+

Site Survey

+
+ + +
+

+ 📷 Site Photos + 0 photos +

+ +
+ +
+ +
📷
+
+ Click to upload or drag and drop
+ JPG, PNG, WebP • Multiple files allowed +
+
+
+ +
+ + +
+ + +
+ + + ⚫ Offline +
+ + +
+
+
+ + +
+
+

🤖 AI Analysis & Recommendations

+

Generate AI-powered network infrastructure recommendations based on survey responses.

+ + + + + +
+ + +
+ +
+ +
+ + + + + + + + +
+
+
+ + + + + + + From baedeba5b738435ea6a905c1d91483bd90e1b508 Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:18:36 -0700 Subject: [PATCH 2/8] Add .gitignore --- .gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 From f668a54e5fac4ccb58762055398cd49cd3fa7383 Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:18:42 -0700 Subject: [PATCH 3/8] Add .gitignore and remove incomplete file --- .gitignore | 15 ++ app_complete.py | 663 ------------------------------------------------ 2 files changed, 15 insertions(+), 663 deletions(-) delete mode 100644 app_complete.py diff --git a/.gitignore b/.gitignore index e69de29..819b98b 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,15 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +venv/ + +# Uploads +uploads/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_complete.py b/app_complete.py deleted file mode 100644 index ebe13d3..0000000 --- a/app_complete.py +++ /dev/null @@ -1,663 +0,0 @@ -#!/usr/bin/env python3 -""" -IT Site Survey AI Application -Complete implementation with all requested features: -- Photo uploads for AI assessment -- Model selection (Ollama vision models) -- Export results (PDF, Text, Email) -- Delete surveys (individual + Clear All) -- Chat interface for AI interaction -- Load Sample Data for testing -- Comprehensive 35-question survey template -""" - -from flask import Flask, request, jsonify, send_file -from flask_cors import CORS -import os -import json -import uuid -from datetime import datetime -import base64 -import requests -from werkzeug.utils import secure_filename - -app = Flask(__name__) -CORS(app, resources={ - r"/api/*": { - "origins": ["http://192.168.50.11:3003", "http://localhost:3003"], - "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": ["Content-Type", "Authorization"] - } -}) - -# Configuration -UPLOAD_FOLDER = 'uploads' -PHOTOS_FOLDER = 'uploads/photos' -ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} -OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.19.25:11434') -OLLAMA_URL = f"{OLLAMA_HOST}/api/chat" - -# Create upload directories -os.makedirs(UPLOAD_FOLDER, exist_ok=True) -os.makedirs(PHOTOS_FOLDER, exist_ok=True) - -# In-memory storage -surveys_db = {} -responses_db = {} - -# Survey Template with 35 comprehensive questions -DEFAULT_SURVEY_TEMPLATE = { - "title": "IT Infrastructure Site Survey", - "description": "Comprehensive network infrastructure assessment", - "questions": [ - # Site Topology & Layout (1-4) - { - "id": "q1", - "category": "Site Topology & Layout", - "question": "What is the total square footage of the site?", - "type": "number", - "placeholder": "e.g., 3500" - }, - { - "id": "q2", - "category": "Site Topology & Layout", - "question": "How many floors or buildings need network coverage?", - "type": "number", - "placeholder": "e.g., 2" - }, - { - "id": "q3", - "category": "Site Topology & Layout", - "question": "Describe the site layout (open plan, offices, warehouse, etc.)", - "type": "textarea", - "placeholder": "Describe the layout..." - }, - { - "id": "q4", - "category": "Site Topology & Layout", - "question": "Are there any physical barriers that may affect WiFi coverage?", - "type": "multiselect", - "options": ["Concrete walls", "Metal shelving", "Elevator shafts", "Thick drywall", "None"] - }, - - # Network Closet & Equipment (5-8) - { - "id": "q5", - "category": "Network Closet & Equipment", - "question": "How many network closets or IDF rooms are on-site?", - "type": "number", - "placeholder": "e.g., 2" - }, - { - "id": "q6", - "category": "Network Closet & Equipment", - "question": "Describe the current network closet setup", - "type": "textarea", - "placeholder": "Describe the setup..." - }, - { - "id": "q7", - "category": "Network Closet & Equipment", - "question": "What type of power and cooling is available in the network closets?", - "type": "textarea", - "placeholder": "Describe power and cooling..." - }, - { - "id": "q8", - "category": "Network Closet & Equipment", - "question": "Are there existing server racks or wall-mounted equipment?", - "type": "select", - "options": ["Yes, full racks", "Yes, partial racks", "Wall-mounted only", "No existing racks"] - }, - - # Cabling Infrastructure (9-12) - { - "id": "q9", - "category": "Cabling Infrastructure", - "question": "What type of existing cabling is in place?", - "type": "select", - "options": ["Cat5e (100Mb-1GbE)", "Cat6 (1GbE-10GbE)", "Cat6a (10GbE)", "Fiber (Multi-mode)", "Fiber (Single-mode)", "No existing cabling", "Unknown"] - }, - { - "id": "q10", - "category": "Cabling Infrastructure", - "question": "How many network drops are currently installed?", - "type": "number", - "placeholder": "e.g., 120" - }, - { - "id": "q11", - "category": "Cabling Infrastructure", - "question": "What is the estimated distance from the MDF to the farthest IDF or workstation?", - "type": "textarea", - "placeholder": "Describe distances..." - }, - { - "id": "q12", - "category": "Cabling Infrastructure", - "question": "Are there any special cabling requirements?", - "type": "multiselect", - "options": ["Plenum-rated cable required", "Outdoor-rated cable needed", "Direct burial cable needed", "Aerial cable needed", "Shielded cable required", "None"] - }, - - # Network Equipment (13-15) - { - "id": "q13", - "category": "Network Equipment", - "question": "What network equipment is currently installed?", - "type": "multiselect", - "options": ["Router/Firewall", "Managed switches", "Unmanaged switches", "Wireless access points", "None/Unknown"] - }, - { - "id": "q14", - "category": "Network Equipment", - "question": "What type of switches are needed?", - "type": "select", - "options": ["Unmanaged (plug-and-play)", "Managed (Layer 2)", "Managed (Layer 3 with routing)", "PoE switches", "Industrial-grade switches"] - }, - { - "id": "q15", - "category": "Network Equipment", - "question": "How many wireless access points are needed?", - "type": "number", - "placeholder": "e.g., 6" - }, - - # Power Requirements (16-18) - { - "id": "q16", - "category": "Power Requirements", - "question": "What is the estimated total power consumption for all network equipment?", - "type": "textarea", - "placeholder": "Describe power requirements..." - }, - { - "id": "q17", - "category": "Power Requirements", - "question": "What PoE devices will be connected?", - "type": "multiselect", - "options": ["IP Phones", "Wireless APs", "IP Cameras", "IoT sensors", "None", "Not sure"] - }, - { - "id": "q18", - "category": "Power Requirements", - "question": "What UPS backup capacity is required?", - "type": "select", - "options": ["No backup needed", "Minimal backup (15-30 min)", "Standard backup (1-2 hours)", "Extended backup (4+ hours)", "Generator backup"] - }, - - # User & Device Requirements (19-21) - { - "id": "q19", - "category": "User & Device Requirements", - "question": "How many employees or users will use the network?", - "type": "number", - "placeholder": "e.g., 45" - }, - { - "id": "q20", - "category": "User & Device Requirements", - "question": "What is the estimated total number of devices?", - "type": "number", - "placeholder": "e.g., 85" - }, - { - "id": "q21", - "category": "User & Device Requirements", - "question": "What types of network activities will be performed?", - "type": "multiselect", - "options": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop", "VoIP/Phone calls", "Streaming media"] - }, - - # Security & Compliance (22-24) - { - "id": "q22", - "category": "Security & Compliance", - "question": "What compliance standards must be met?", - "type": "multiselect", - "options": ["HIPAA", "PCI-DSS", "SOC 2", "GDPR", "NIST", "None/General security"] - }, - { - "id": "q23", - "category": "Security & Compliance", - "question": "What network segmentation is required?", - "type": "multiselect", - "options": ["Corporate network", "Guest network", "IoT network", "Management network", "No segmentation needed"] - }, - { - "id": "q24", - "category": "Security & Compliance", - "question": "What security features are needed?", - "type": "multiselect", - "options": ["Firewall", "VPN access", "Intrusion detection", "Network access control (NAC)", "Content filtering", "None"] - }, - - # Internet & WAN (25-27) - { - "id": "q25", - "category": "Internet & WAN", - "question": "What type of internet connectivity is available?", - "type": "multiselect", - "options": ["Fiber", "Cable/DSL", "5G/Cellular backup", "Dedicated leased line", "No existing connection"] - }, - { - "id": "q26", - "category": "Internet & WAN", - "question": "What is the required internet bandwidth?", - "type": "textarea", - "placeholder": "e.g., 500Mbps download / 250Mbps upload" - }, - { - "id": "q27", - "category": "Internet & WAN", - "question": "Are there multiple sites that need to be connected?", - "type": "select", - "options": ["No - Single site only", "Yes - 2-3 sites", "Yes - 4+ sites", "Not sure yet"] - }, - - # Existing Infrastructure (28-30) - { - "id": "q28", - "category": "Existing Infrastructure", - "question": "Is there existing IT infrastructure to integrate with?", - "type": "select", - "options": ["Yes - Full upgrade needed", "Yes - Partial upgrade", "No - New installation", "Not sure"] - }, - { - "id": "q29", - "category": "Existing Infrastructure", - "question": "Describe any existing network issues or limitations", - "type": "textarea", - "placeholder": "Describe current issues..." - }, - { - "id": "q30", - "category": "Existing Infrastructure", - "question": "What performance issues have been observed?", - "type": "multiselect", - "options": ["Slow internet", "WiFi dead zones", "Network congestion", "Frequent disconnections", "No issues observed"] - }, - - # Budget & Timeline (31-33) - { - "id": "q31", - "category": "Budget & Timeline", - "question": "What is the estimated budget for this project?", - "type": "select", - "options": ["Under $10,000", "$10,000 - $25,000", "$25,000 - $50,000", "$50,000 - $100,000", "$100,000+", "Not determined yet"] - }, - { - "id": "q32", - "category": "Budget & Timeline", - "question": "What is the desired timeline for completion?", - "type": "select", - "options": ["Within 1 month", "Within 3 months", "Within 6 months", "Within 1 year", "Flexible"] - }, - { - "id": "q33", - "category": "Budget & Timeline", - "question": "Who will be responsible for ongoing network management?", - "type": "select", - "options": ["Internal IT team", "Managed service provider (MSP)", "Hybrid approach", "Not determined yet"] - }, - - # Additional Details (34-35) - { - "id": "q34", - "category": "Additional Details", - "question": "Upload any site photos, floor plans, or network diagrams", - "type": "file", - "placeholder": "Upload photos..." - }, - { - "id": "q35", - "category": "Additional Details", - "question": "Any additional requirements or special considerations?", - "type": "textarea", - "placeholder": "Describe any additional requirements..." - } - ] -} - -# Sample data for testing -SAMPLE_SURVEY_DATA = { - "name": "Sample Corporate Office Survey", - "client_name": "Acme Corporation", - "site_name": "Main Office - Downtown", - "description": "Comprehensive network infrastructure assessment for downtown office location", - "responses": { - "q1": "3500", - "q2": "2", - "q3": "Single-story office building with open floor plan, 8 private offices, 2 conference rooms, break room, and reception area. Approximately 60 workstations in cubicle area.", - "q4": ["Concrete walls"], - "q5": "2", - "q6": "MDF located in basement utility room. IDF on first floor in IT storage closet. Both have adequate ventilation but limited space.", - "q7": "2x 20A circuits per closet, UPS backup planned for MDF", - "q8": "Yes, partial racks", - "q9": "Cat5e (100Mb-1GbE)", - "q10": "120", - "q11": "Average 150 feet, longest run approximately 280 feet to corner offices", - "q12": ["Plenum-rated cable required"], - "q13": ["Router/Firewall", "Unmanaged switches", "Wireless access points"], - "q14": "Managed (Layer 2)", - "q15": "6", - "q16": "1800W estimated for all network equipment including PoE devices", - "q17": ["IP Phones", "Wireless APs", "IP Cameras"], - "q18": "Limited backup (1-2 hours)", - "q19": "45", - "q20": "85", - "q21": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop"], - "q22": ["SOC 2"], - "q23": ["Corporate network", "Guest network", "IoT network", "Management network"], - "q24": ["Firewall", "VPN access", "Network access control (NAC)"], - "q25": ["Fiber", "5G/Cellular backup"], - "q26": "500Mbps download / 250Mbps upload", - "q27": "No - Single site only", - "q28": "Yes - Partial upgrade", - "q29": "Current WiFi has dead zones in conference rooms. Existing switches are unmanaged and showing signs of age. Need better coverage and management capabilities.", - "q30": ["Slow internet", "WiFi dead zones", "Network congestion"], - "q31": "$25,000 - $50,000", - "q32": "Within 3 months", - "q33": "Internal IT team", - "q34": "", - "q35": "Need outdoor WiFi coverage for patio area. Plan to add IP cameras in future. Prefer Cisco or Ubiquiti equipment. Require weekend installation to minimize downtime." - } -} - - -@app.route('/api/health', methods=['GET']) -def health_check(): - """Health check endpoint""" - try: - resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) - ollama_online = resp.status_code == 200 - except: - ollama_online = False - - return jsonify({ - "status": "ok", - "service": "site-survey-ai", - "timestamp": datetime.utcnow().isoformat(), - "ollama_connected": ollama_online - }) - - -@app.route('/api/models', methods=['GET']) -def get_models(): - """Get available Ollama models""" - try: - resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) - if resp.status_code == 200: - models_data = resp.json() - models = [m['name'] for m in models_data.get('models', [])] - # Add cloud models if available - cloud_models = ['kimi-k2.6:cloud', 'kimi-k2.5:cloud', 'nemotron-3-super:cloud'] - models.extend(cloud_models) - return jsonify({"models": models, "default": "kimi-k2.6:cloud"}) - except Exception as e: - print(f"Error fetching models: {e}") - - return jsonify({ - "models": ["kimi-k2.6:cloud", "kimi-k2.5:cloud", "llava:latest"], - "default": "kimi-k2.6:cloud" - }) - - -@app.route('/api/ollama-status', methods=['GET']) -def ollama_status(): - """Check Ollama connection status""" - try: - resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5) - return jsonify({"online": resp.status_code == 200}) - except: - return jsonify({"online": False}) - - -@app.route('/api/surveys/template', methods=['GET']) -def get_survey_template(): - """Get the survey template with questions""" - return jsonify({"template": DEFAULT_SURVEY_TEMPLATE}) - - -@app.route('/api/surveys', methods=['GET']) -def list_surveys(): - """List all surveys""" - surveys = [] - for sid, survey in surveys_db.items(): - surveys.append({ - "id": sid, - "name": survey['name'], - "client_name": survey.get('client_name', ''), - "site_name": survey.get('site_name', ''), - "description": survey.get('description', ''), - "created_at": survey['created_at'], - "responses_count": len(survey.get('responses', [])) - }) - return jsonify({"surveys": surveys}) - - -@app.route('/api/surveys', methods=['POST']) -def create_survey(): - """Create a new survey""" - data = request.get_json() - - survey_id = str(uuid.uuid4()) - survey = { - 'id': survey_id, - 'name': data.get('name', 'New Survey'), - 'description': data.get('description', ''), - 'client_name': data.get('client_name', ''), - 'site_name': data.get('site_name', ''), - 'questions': DEFAULT_SURVEY_TEMPLATE['questions'], - 'created_at': datetime.utcnow().isoformat(), - 'status': 'active', - 'responses': [] - } - - # If responses are provided, add them - responses_data = data.get('responses') - if responses_data and isinstance(responses_data, dict) and len(responses_data) > 0: - response_entry = { - 'id': str(uuid.uuid4()), - 'survey_id': survey_id, - 'responses': responses_data, - 'photos': [], - 'analyzed': False, - 'analysis': None, - 'submitter_name': 'Survey Creator', - 'created_at': datetime.utcnow().isoformat() - } - survey['responses'].append(response_entry) - - surveys_db[survey_id] = survey - return jsonify({"survey": survey}), 201 - - -@app.route('/api/surveys/', methods=['GET']) -def get_survey(survey_id): - """Get a specific survey""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - return jsonify({"survey": surveys_db[survey_id]}) - - -@app.route('/api/surveys/', methods=['DELETE']) -def delete_survey(survey_id): - """Delete a survey""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - - del surveys_db[survey_id] - return jsonify({"success": True, "message": "Survey deleted"}) - - -@app.route('/api/surveys', methods=['DELETE']) -def delete_all_surveys(): - """Delete all surveys""" - count = len(surveys_db) - surveys_db.clear() - return jsonify({"success": True, "message": f"Deleted {count} surveys"}) - - -@app.route('/api/surveys//responses', methods=['POST']) -def submit_response(survey_id): - """Submit survey responses""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - - data = request.get_json() - response_id = str(uuid.uuid4()) - - response_entry = { - 'id': response_id, - 'survey_id': survey_id, - 'responses': data.get('responses', {}), - 'photos': data.get('photos', []), - 'analyzed': False, - 'analysis': None, - 'submitter_name': data.get('submitter_name', 'Anonymous'), - 'created_at': datetime.utcnow().isoformat() - } - - surveys_db[survey_id]['responses'].append(response_entry) - return jsonify({"response": response_entry}), 201 - - -@app.route('/api/surveys//photos', methods=['POST']) -def upload_photo(survey_id): - """Upload a photo for a survey""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - - if 'photo' not in request.files: - return jsonify({"error": "No photo provided"}), 400 - - photo = request.files['photo'] - if photo.filename == '': - return jsonify({"error": "No photo selected"}), 400 - - # Save photo - filename = secure_filename(f"{survey_id}_{uuid.uuid4()}_{photo.filename}") - photo_path = os.path.join(PHOTOS_FOLDER, filename) - photo.save(photo_path) - - return jsonify({ - "success": True, - "filename": filename, - "url": f"/api/photos/{survey_id}/{filename}" - }) - - -@app.route('/api/photos//', methods=['GET']) -def get_photo(survey_id, filename): - """Get a photo""" - photo_path = os.path.join(PHOTOS_FOLDER, filename) - if os.path.exists(photo_path): - return send_file(photo_path) - return jsonify({"error": "Photo not found"}), 404 - - -@app.route('/api/surveys//analyze', methods=['POST']) -def analyze_survey(survey_id): - """Analyze survey with AI""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - - survey = surveys_db[survey_id] - data = request.get_json() or {} - selected_model = data.get('model', 'llava:latest') - - # Get the latest response - if not survey['responses']: - return jsonify({"error": "No responses to analyze"}), 400 - - last_response = survey['responses'][-1] - responses = last_response['responses'] - - # Build analysis prompt - prompt = f"""Analyze this IT site survey and provide recommendations: - -Survey: {survey['name']} -Client: {survey['client_name']} -Site: {survey['site_name']} - -Responses: -""" - - for q in survey['questions']: - qid = q['id'] - if qid in responses: - prompt += f"\n{q['question']}: {responses[qid]}" - - prompt += "\n\nProvide a detailed analysis including:\n" - prompt += "1. Network infrastructure recommendations\n" - prompt += "2. Equipment recommendations with estimated costs\n" - prompt += "3. Implementation timeline\n" - prompt += "4. Potential challenges and solutions\n" - - try: - # Call Ollama for analysis - ollama_data = { - "model": selected_model, - "messages": [{"role": "user", "content": prompt}], - "stream": False - } - - resp = requests.post(OLLAMA_URL, json=ollama_data, timeout=300) - if resp.status_code == 200: - result = resp.json() - analysis = result.get('message', {}).get('content', 'No analysis available') - - # Save analysis - last_response['analyzed'] = True - last_response['analysis'] = analysis - last_response['model_used'] = selected_model - - return jsonify({ - "analysis": analysis, - "model_used": selected_model - }) - else: - return jsonify({"error": f"Ollama error: {resp.status_code}"}), 500 - - except Exception as e: - return jsonify({"error": str(e)}), 500 - - -@app.route('/api/surveys//export', methods=['POST']) -def export_survey(survey_id): - """Export survey results""" - if survey_id not in surveys_db: - return jsonify({"error": "Survey not found"}), 404 - - data = request.get_json() or {} - export_format = data.get('format', 'json') - - survey = surveys_db[survey_id] - - if export_format == 'json': - return jsonify({"survey": survey}) - elif export_format == 'text': - # Generate text report - text = f"IT Site Survey Report\n" - text += f"Survey: {survey['name']}\n" - text += f"Client: {survey['client_name']}\n" - text += f"Site: {survey['site_name']}\n" - text += f"Date: {survey['created_at']}\n\n" - - for response in survey['responses']: - text += f"Response by: {response['submitter_name']}\n" - for q in survey['questions']: - qid = q['id'] - if qid in response['responses']: - text += f"{q['question']}: {response['responses'][qid]}\n" - if response.get('analysis'): - text += f"\nAI Analysis:\n{response['analysis']}\n" - - return jsonify({"text": text}) - else: - return jsonify({"error": "Unsupported format"}), 400 - - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=3003, debug=False) From d79f806c596bc5309cae9df36f5e3b0221c24d91 Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:26:22 -0700 Subject: [PATCH 4/8] Fix: Move Load Sample Data button to Take Survey tab where questions exist --- index.html | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/index.html b/index.html index 2e5fbe3..ae844e6 100644 --- a/index.html +++ b/index.html @@ -500,7 +500,6 @@ n .btn-secondary {

➕ Create New Site Survey

-
@@ -532,6 +531,7 @@ n .btn-secondary {

Site Survey

+
@@ -1439,12 +1439,6 @@ n .btn-secondary { console.log('Loading sample data...'); - // Set survey metadata - document.getElementById('surveyName').value = 'Sample Corporate Office Survey'; - document.getElementById('clientName').value = 'Acme Corporation'; - document.getElementById('siteName').value = 'Main Office - Downtown'; - document.getElementById('surveyDescription').value = 'Comprehensive network infrastructure assessment for downtown office location'; - // Sample data for all questions (keyed by question number like 'q1', 'q2') const sampleValues = { 'q1': '3500', From 7762c9f3086b54225dc5c9cf5f523c0a72ddcd50 Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:26:56 -0700 Subject: [PATCH 5/8] Fix: Improve createSurvey error handling and user flow --- index.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index ae844e6..54b614c 100644 --- a/index.html +++ b/index.html @@ -921,9 +921,6 @@ n .btn-secondary { return; } - // Check if we have question responses - const hasResponses = Object.keys(createSurveyResponses).length > 0; - try { const response = await fetch('/api/surveys', { method: 'POST', @@ -932,23 +929,32 @@ n .btn-secondary { name, client_name: clientName, site_name: siteName, - description, - responses: hasResponses ? createSurveyResponses : undefined + description }) }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to create survey'); + } + const data = await response.json(); + // Clear form document.getElementById('surveyName').value = ''; document.getElementById('clientName').value = ''; document.getElementById('siteName').value = ''; document.getElementById('surveyDescription').value = ''; await loadSurveys(); - loadSurvey(data.survey.id); + + // Switch to surveys tab to show the new survey + switchTab('surveys'); + alert('Survey created successfully! Click on it to take the survey.'); } catch (e) { - alert('Failed to create survey'); + console.error('Error creating survey:', e); + alert('Failed to create survey: ' + e.message); } } From 6f28a9e40c3ded87703ab376edbc2fc0f88483fc Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:28:12 -0700 Subject: [PATCH 6/8] Fix: Add clearAllSurveys function that was missing --- index.html | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 54b614c..b9a2032 100644 --- a/index.html +++ b/index.html @@ -1567,7 +1567,28 @@ function confirmClearAll() { } } - +async function clearAllSurveys() { + try { + const response = await fetch('/api/surveys', { + method: 'DELETE' + }); + const data = await response.json(); + + if (data.success) { + showNotification('All surveys deleted successfully', 'success'); + loadSurveys(); + currentSurveyId = null; + currentSurvey = null; + document.getElementById('takeSurveyTab').style.display = 'none'; + document.getElementById('analyzeTab').style.display = 'none'; + } else { + showNotification(data.error || 'Failed to clear surveys', 'error'); + } + } catch (e) { + console.error('Error clearing surveys:', e); + showNotification('Failed to clear surveys', 'error'); + } +} // Notification helper function showNotification(message, type = 'info') { From e0654429dc4e4cfe6f48936cab9a34625e0a668f Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:34:53 -0700 Subject: [PATCH 7/8] Rebuild: Clean single-page survey with all features integrated --- index.html | 1631 +++++++++++++++------------------------------------- 1 file changed, 456 insertions(+), 1175 deletions(-) diff --git a/index.html b/index.html index b9a2032..d06d00a 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - IT Site Survey AI | Network Infrastructure Planning + IT Site Survey AI +
- +

IT Site Survey AI

-

AI-powered network infrastructure planning & recommendations

+

Network Infrastructure Planning & Analysis

@@ -482,14 +291,16 @@ n .btn-secondary {
-
-

📋 Site Surveys

+
+

📋 Site Surveys

+ +
Loading surveys...
@@ -500,6 +311,8 @@ n .btn-secondary {

➕ Create New Site Survey

+ +
@@ -521,72 +334,39 @@ n .btn-secondary {
-
- -
-
+
- -
-
-
-

Site Survey

- -
- - -
-

- 📷 Site Photos - 0 photos -

- -
- -
- -
📷
-
- Click to upload or drag and drop
- JPG, PNG, WebP • Multiple files allowed -
-
+
+

📷 Site Photos

+
+
+ +
📷
+
Click to upload or drag and drop
+ JPG, PNG, WebP • Multiple files allowed
+
-
- - -
- - -
- - - ⚫ Offline -
+
+ + +
+
+
-
+

🤖 AI Analysis & Recommendations

Generate AI-powered network infrastructure recommendations based on survey responses.

- - - -
- + @@ -599,398 +379,108 @@ n .btn-secondary {
- - - - - - - + // Load models + async function loadModels() { + try { + const resp = await fetch('/api/models'); + const data = await resp.json(); + availableModels = data.models || []; + defaultModel = data.default || 'llava:latest'; + + const analyzeSelect = document.getElementById('analyzeModelSelect'); + analyzeSelect.innerHTML = availableModels.map(m => + `` + ).join(''); + + } catch (e) { + console.error('Failed to load models:', e); + } + } + From b552f27f52ff3015f84173335bbc344b8e8702c2 Mon Sep 17 00:00:00 2001 From: JC Beasley Date: Thu, 2 Jul 2026 15:52:48 -0700 Subject: [PATCH 8/8] Add chat endpoint, model selector, and improved AI analysis --- app.py | 35 +++++++++++ index.html | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 7715876..3b28ffc 100644 --- a/app.py +++ b/app.py @@ -966,6 +966,41 @@ Report generated by IT Site Survey AI
return jsonify({'email_body': email_body}) +@app.route("/api/surveys//chat", methods=["POST"]) +def chat_with_ai(survey_id): + survey = surveys_db.get(survey_id) + if not survey: + return jsonify({"error": "Survey not found"}), 404 + + data = request.get_json() or {} + user_message = data.get("message", "") + chat_history = data.get("history", []) + model = data.get("model", DEFAULT_MODEL) + + latest_response = survey["responses"][-1] if survey["responses"] else None + responses = latest_response["responses"] if latest_response else {} + + survey_context = f"Client: {survey.get("client_name", "Unknown")}\nSite: {survey.get("site_name", "Unknown")}\n\nSurvey Responses:\n" + for q in survey["questions"]: + qid = q["id"] + answer = responses.get(qid, "Not answered") + survey_context += f"{q["question"]}: {answer}\n" + + messages = [{"role": "system", "content": f"You are a network infrastructure expert reviewing this survey: {survey_context}"}] + for msg in chat_history: + messages.append(msg) + messages.append({"role": "user", "content": user_message}) + + try: + resp = requests.post(f"{OLLAMA_BASE}/api/chat", json={"model": model, "messages": messages, "stream": False}, timeout=120) + if resp.status_code == 200: + result = resp.json() + return jsonify({"response": result.get("message", {}).get("content", "No response")}) + else: + return jsonify({"error": f"Ollama error: {resp.status_code}"}), 500 + except Exception as e: + return jsonify({"error": str(e)}), 500 + if __name__ == '__main__': port = int(os.environ.get('PORT', 3003)) print("───────────────────────────────────────") diff --git a/index.html b/index.html index d06d00a..93a2edd 100644 --- a/index.html +++ b/index.html @@ -334,6 +334,13 @@
+
+ + +
+
@@ -392,6 +399,18 @@
+ + +
@@ -881,6 +900,135 @@ } } + // Chat functionality + let chatHistory = []; + + async function sendChatMessage() { + const input = document.getElementById('chatInput'); + const messagesDiv = document.getElementById('chatMessages'); + const message = input.value.trim(); + + if (!message) return; + if (!currentSurvey) { + alert('Please select a survey first'); + return; + } + + // Add user message + chatHistory.push({role: 'user', content: message}); + appendChatMessage('You', message, 'user'); + input.value = ''; + + try { + const modelSelect = document.getElementById('analyzeModelSelect'); + const selectedModel = modelSelect.value || defaultModel; + + const response = await fetch(`/api/surveys/${currentSurveyId}/chat`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + message: message, + history: chatHistory, + model: selectedModel + }) + }); + + const data = await response.json(); + + if (data.response) { + chatHistory.push({role: 'assistant', content: data.response}); + appendChatMessage('AI', data.response, 'assistant'); + } else { + appendChatMessage('Error', data.error || 'Failed to get response', 'error'); + } + + } catch (e) { + console.error('Chat error:', e); + appendChatMessage('Error', 'Failed to send message', 'error'); + } + } + + function appendChatMessage(sender, text, type) { + const messagesDiv = document.getElementById('chatMessages'); + const messageDiv = document.createElement('div'); + messageDiv.style.cssText = ` + margin-bottom: 10px; + padding: 10px; + border-radius: var(--radius); + background: ${type === 'user' ? 'rgba(88, 166, 255, 0.1)' : type === 'error' ? 'rgba(248, 81, 73, 0.1)' : 'var(--bg-3)'}; + border-left: 3px solid ${type === 'user' ? 'var(--accent)' : type === 'error' ? 'var(--danger)' : 'var(--success)'}; + `; + messageDiv.innerHTML = ` +
${sender}
+
${text}
+ `; + messagesDiv.appendChild(messageDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + } + + // Improved AI Analysis with streaming + async function runAIAnalysis() { + if (!currentSurveyId) { + alert('Please select a survey first'); + return; + } + + const modelSelect = document.getElementById('analyzeModelSelect'); + const selectedModel = modelSelect.value || defaultModel; + + document.getElementById('analyzeSpinner').style.display = 'inline-block'; + document.getElementById('analysisResult').style.display = 'block'; + document.getElementById('analysisContent').textContent = 'Generating analysis...'; + + try { + const response = await fetch(`/api/surveys/${currentSurveyId}/analyze`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({model: selectedModel}) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let analysisText = ''; + + while (true) { + const {done, value} = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.trim()) { + try { + const data = JSON.parse(line); + if (data.response) { + analysisText += data.response; + document.getElementById('analysisContent').textContent = analysisText; + } + if (data.done) { + document.getElementById('analysisExport').style.display = 'block'; + document.getElementById('chatInterface').style.display = 'block'; + // Clear chat history for new analysis + chatHistory = []; + document.getElementById('chatMessages').innerHTML = ''; + appendChatMessage('AI', 'Analysis complete! You can now ask questions about the recommendations.', 'assistant'); + } + } catch (e) { + // Ignore parse errors for incomplete chunks + } + } + } + } + + } catch (e) { + console.error('Analysis error:', e); + document.getElementById('analysisContent').textContent = 'Error: ' + e.message; + } finally { + document.getElementById('analyzeSpinner').style.display = 'none'; + } + } + // Load models async function loadModels() { try { @@ -890,14 +1038,31 @@ defaultModel = data.default || 'llava:latest'; const analyzeSelect = document.getElementById('analyzeModelSelect'); - analyzeSelect.innerHTML = availableModels.map(m => + const createSelect = document.getElementById('createModelSelect'); + + const options = availableModels.map(m => `` ).join(''); + analyzeSelect.innerHTML = options; + createSelect.innerHTML = options; + } catch (e) { console.error('Failed to load models:', e); } } + + // Handle Enter key in chat + document.addEventListener('DOMContentLoaded', () => { + const chatInput = document.getElementById('chatInput'); + if (chatInput) { + chatInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendChatMessage(); + } + }); + } + });