Update memory service with working NocoDB configuration

- Set correct table ID: mx149yctebfwvys (ai_data_Memory)
- Updated type column with SingleSelect options
- Added all required columns for corrections, preferences, episodes, decisions, validation
- Fixed column options for type, severity, status fields
This commit is contained in:
JC Beasley
2026-07-04 17:02:40 -07:00
parent cf3f1e1c53
commit cb5e26b561
453 changed files with 386793 additions and 64 deletions
@@ -0,0 +1,170 @@
# IT Site Survey AI - Code Structure Analysis
## Overview
The application is a Flask-based web application that provides IT infrastructure site survey functionality with AI-powered analysis and recommendations.
## Main Components
### 1. Core Application (app.py)
- **Lines of Code**: 792
- **Framework**: Flask
- **Key Dependencies**: requests, reportlab, flask-cors
- **External Services**: Ollama AI at http://192.168.19.25:11434
### 2. Data Model
- **Template**: concise_template.json (21 questions across 5 categories)
- **In-Memory Storage**:
- `surveys_db` - Dictionary of survey definitions
- `survey_responses_db` - Dictionary of survey responses
- **Data Structure**:
- Surveys with metadata and questions
- Responses with submission data and answers
### 3. Key Routes
#### Survey Management
- `GET /api/surveys/template` - Get survey template
- `GET /api/surveys` - List all surveys
- `POST /api/surveys` - Create new survey
- `GET /api/surveys/<survey_id>` - Get specific survey
#### Response Handling
- `POST /api/surveys/<survey_id>/responses` - Submit survey response
- `GET /api/surveys/responses` - Get all responses (recently added)
#### AI Analysis
- `POST /api/surveys/<survey_id>/analyze` - Generate AI recommendations
- `POST /api/surveys/<survey_id>/generate-quote` - Generate service quote
#### Export Functions
- `POST /api/surveys/<survey_id>/export/pdf` - Export to PDF
- `POST /api/surveys/<survey_id>/export/text` - Export to text
- `POST /api/surveys/<survey_id>/export/email` - Export to email format
#### System
- `GET /` - Main application page
- `GET /<path:path>` - Static file serving
- `GET /api/health` - Health check
- `GET /api/ollama-status` - Ollama connectivity check
### 4. Frontend Files
- `index.html` - Main survey interface (18,263 bytes)
- `dashboard.html` - Response dashboard (337 bytes)
### 5. Template Structure
**21 Questions Across 5 Categories:**
1. **Client Information** (2 questions)
- Company Name (text)
- Industry/Vertical (select)
2. **Site Information** (3 questions)
- Site Size (select)
- Building Type (select)
- Square Footage (select)
3. **Existing Infrastructure** (4 questions)
- Network Topology (select)
- Network Size (select)
- Internet Bandwidth (select)
- ISP Connection Type (multiselect)
4. **Wireless Infrastructure** (2 questions)
- Wireless Standard (select)
- Wireless Coverage Quality (select)
5. **Business Requirements** (10 questions)
- Critical Applications (multiselect)
- Device Types (multiselect)
- User Density (select)
- Performance Requirements (multiselect)
- Security Requirements (multiselect)
- Compliance Requirements (select)
- Budget Range (select)
- Project Timeline (select)
- Additional Notes (textarea)
## Current Limitations
### 1. Data Persistence
- **Issue**: All data stored in memory (lost on restart)
- **Impact**: Critical for production use
- **Solution Needed**: Database persistence
### 2. Security
- **Issue**: No authentication on dashboard
- **Impact**: Survey data publicly accessible
- **Solution Needed**: User authentication
### 3. Scalability
- **Issue**: Single-process Flask application
- **Impact**: Limited concurrent users
- **Solution Needed**: Multi-process or async handling
## Database Schema Requirements
For PostgreSQL implementation, the following tables would be needed:
### surveys
- id (UUID, PK)
- name (VARCHAR)
- description (TEXT)
- client_name (VARCHAR)
- site_name (VARCHAR)
- created_at (TIMESTAMP)
- status (VARCHAR)
### survey_questions
- id (UUID, PK)
- survey_id (UUID, FK)
- question_id (VARCHAR)
- category (VARCHAR)
- question_text (TEXT)
- question_type (VARCHAR)
- options (JSON)
### survey_responses
- id (UUID, PK)
- survey_id (UUID, FK)
- submitted_by (VARCHAR)
- submitted_at (TIMESTAMP)
### response_answers
- id (UUID, PK)
- response_id (UUID, FK)
- question_id (VARCHAR)
- answer_value (TEXT or JSON)
## Implementation Considerations
### 1. Backward Compatibility
- All existing API endpoints must continue working
- No breaking changes to data structure
- Maintain same JSON response formats
### 2. Migration Strategy
- Seamless transition from memory to database
- No data loss during migration
- Fallback to memory if database unavailable
### 3. Configuration
- Database connection via environment variables
- Default to memory storage if no DB configured
- Clear setup instructions for PostgreSQL
### 4. Error Handling
- Graceful degradation if database unavailable
- Clear error messages for connectivity issues
- Logging for debugging database issues
## Dependencies to Add
- `psycopg2-binary` - PostgreSQL driver
- `sqlalchemy` - ORM (optional but recommended)
- Database connection pooling
## Estimated Implementation Effort
- **Schema Design**: 2 hours
- **Database Integration**: 6 hours
- **Migration Logic**: 3 hours
- **Testing**: 3 hours
- **Documentation**: 2 hours
- **Total**: 16 hours
@@ -0,0 +1,51 @@
# IT Site Survey AI - Enhancements Summary
## Current Status
The application is running at http://192.168.50.11:3003/ with the following enhancements:
### 1. API Endpoint for Survey Responses
- Added new API endpoint: `/api/surveys/responses` (GET)
- Returns JSON array of all submitted survey responses
- Located in app.py lines 778-782
### 2. Dashboard Interface
- Created dashboard.html for viewing survey responses
- Accessible at http://192.168.50.11:3003/dashboard.html
- Displays responses in a structured format with submission metadata
- Includes navigation back to the main survey page
### 3. Enhanced Main Survey Page
- Added "Dashboard" button for easy access to response viewing
- Located in the header section of index.html
### 4. Technical Implementation
- Application running on port 3003
- Process ID: 14287 (as of last check)
- All endpoints functional and tested
## Files Modified
- `app.py` - Added get_all_responses() API endpoint
- `index.html` - Added dashboard link in header
- `dashboard.html` - Created dashboard interface
- `ENHANCEMENTS_SUMMARY.md` - This file
## Testing Verification
- API endpoint returns empty array when no responses exist
- Dashboard loads and connects to API endpoint
- Main survey page accessible with dashboard link
## Next Steps
- Consider adding authentication for dashboard access
- Implement data persistence to database instead of in-memory storage
- Add filtering and search capabilities to dashboard
- Enhance dashboard with charts and analytics
## Memory System
As of July 3, 2026, a memory system has been implemented to address context persistence between sessions:
- STATUS.md: Current project status
- DECISIONS.md: Technical decisions log
- ISSUES.md: Known issues and workarounds
- RUNBOOK.md: Operations procedures
- CHANGELOG.md: Development history
This memory system will help maintain context between conversation sessions and provide better continuity for ongoing development.
@@ -0,0 +1,93 @@
# Task: Implement Database Persistence
## Description
Implement PostgreSQL database persistence for the IT Site Survey AI application. Currently, all survey data is stored in memory and lost when the application restarts.
## Requirements
1. Design a database schema for surveys and responses
2. Add PostgreSQL integration to the Flask application
3. Migrate existing in-memory storage to database storage
4. Ensure all existing API endpoints continue to work
5. Add database connection configuration
6. Update documentation with database setup instructions
## Technical Details
- Application location: /home/jcbeasley/.openclaw/workspace/Projects/site-survey-ai/
- Main application file: app.py
- Current in-memory storage: surveys_db and survey_responses_db dictionaries
- Database: PostgreSQL (install if needed)
- Connection should be configurable via environment variables
## Database Schema
### surveys
- id (UUID, PK)
- name (VARCHAR)
- description (TEXT)
- client_name (VARCHAR)
- site_name (VARCHAR)
- created_at (TIMESTAMP)
- status (VARCHAR)
### survey_questions
- id (UUID, PK)
- survey_id (UUID, FK)
- question_id (VARCHAR)
- category (VARCHAR)
- question_text (TEXT)
- question_type (VARCHAR)
- options (JSON)
### survey_responses
- id (UUID, PK)
- survey_id (UUID, FK)
- submitted_by (VARCHAR)
- submitted_at (TIMESTAMP)
### response_answers
- id (UUID, PK)
- response_id (UUID, FK)
- question_id (VARCHAR)
- answer_value (TEXT or JSON)
## Implementation Considerations
### Backward Compatibility
- All existing API endpoints must continue working
- No breaking changes to data structure
- Maintain same JSON response formats
### Migration Strategy
- Seamless transition from memory to database
- No data loss during migration
- Fallback to memory if database unavailable
### Configuration
- Database connection via environment variables
- Default to memory storage if no DB configured
- Clear setup instructions for PostgreSQL
### Error Handling
- Graceful degradation if database unavailable
- Clear error messages for connectivity issues
- Logging for debugging database issues
## Dependencies to Add
- `psycopg2-binary` - PostgreSQL driver
- `sqlalchemy` - ORM (optional but recommended)
- Database connection pooling
## Acceptance Criteria
- [ ] Survey data persists across application restarts
- [ ] All existing API endpoints continue to function identically
- [ ] Database schema documented
- [ ] Setup instructions added to README
- [ ] No data loss during migration
- [ ] Error handling for database connectivity issues
- [ ] Fallback to memory storage if database unavailable
## Priority
High - Critical for production deployment
## Estimated Effort
16 hours
@@ -0,0 +1,168 @@
# Task: IT Site Survey AI Structure Improvement
## Description
Restructure the IT Site Survey AI application to follow professional Python project standards with clear separation of concerns, modular design, and improved maintainability.
## Current Issues
- Monolithic app.py file (792 lines) containing all application logic
- Mixed concerns (API, business logic, data models, utilities)
- Poor organization making code difficult to navigate
- Backup files cluttering the directory
- No clear separation between frontend and backend
- Missing comprehensive test suite
- Inconsistent with other applications on the server
## Requirements
### 1. Directory Structure
Implement the standardized structure defined in PROJECT_STRUCTURE_IMPROVEMENTS.md:
- src/ directory for Python source code
- Clear separation of models, API, services, and utilities
- Dedicated frontend directory for UI assets
- tests/ directory for comprehensive test suite
- docs/ directory for documentation
- requirements/ directory for dependency management
### 2. Code Modularization
Split the monolithic app.py into logical modules:
- models/ - Data models and database schemas
- api/ - API endpoints organized by resource
- services/ - Business logic separated from API layer
- utils/ - Utility functions and helpers
- config.py - Configuration management
### 3. Testing Implementation
Create comprehensive test suite:
- Unit tests for each module
- Integration tests for API endpoints
- Database integration tests
- Test configuration and fixtures
### 4. Documentation
Create detailed documentation:
- API documentation
- Architecture overview
- Deployment instructions
- Development setup guide
### 5. Development Tools
Set up professional development environment:
- Code linting with Flake8
- Code formatting with Black
- Makefile for common tasks
- Git configuration and .gitignore
## Technical Details
### Source Directory Structure
```
src/
├── __init__.py
├── app.py # Application entry point
├── config.py # Configuration management
├── models/
│ ├── __init__.py
│ ├── survey.py # Survey data model
│ └── response.py # Response data model
├── api/
│ ├── __init__.py
│ ├── surveys.py # Survey management endpoints
│ ├── responses.py # Response handling endpoints
│ └── analytics.py # AI analysis endpoints
├── services/
│ ├── __init__.py
│ ├── survey_service.py # Survey business logic
│ ├── response_service.py # Response business logic
│ ├── ai_service.py # AI integration logic
│ └── export_service.py # Export functionality
├── utils/
│ ├── __init__.py
│ ├── database.py # Database utilities
│ └── validation.py # Input validation
└── templates/ # HTML templates
```
### Frontend Directory Structure
```
frontend/
├── static/
│ ├── css/
│ │ └── styles.css # Main stylesheet
│ ├── js/
│ │ ├── app.js # Main JavaScript
│ │ └── dashboard.js # Dashboard JavaScript
│ └── images/ # Image assets
└── templates/
├── base.html # Base template
├── index.html # Main survey page
└── dashboard.html # Dashboard page
```
### Test Directory Structure
```
tests/
├── __init__.py
├── conftest.py # Test configuration
├── test_models.py # Model tests
├── test_api.py # API endpoint tests
├── test_services.py # Service layer tests
└── test_utils.py # Utility function tests
```
## Implementation Steps
### Phase 1: Directory Restructuring (2 days)
1. Create new directory structure
2. Move existing files to appropriate locations
3. Update import statements
4. Verify application functionality
### Phase 2: Code Modularization (3 days)
1. Split app.py into logical modules
2. Refactor API endpoints into separate files
3. Extract business logic into service layer
4. Create data models
5. Implement configuration management
### Phase 3: Testing Implementation (2 days)
1. Set up testing framework (pytest)
2. Create unit tests for each module
3. Implement integration tests
4. Add test configuration
### Phase 4: Documentation and Tools (1 day)
1. Create API documentation
2. Write architecture overview
3. Set up development tools (Flake8, Black)
4. Create Makefile for common tasks
## Acceptance Criteria
- [ ] Application runs identically to current version
- [ ] All existing API endpoints function correctly
- [ ] Code is organized in logical modules
- [ ] Comprehensive test suite with >80% coverage
- [ ] Documentation covers all major components
- [ ] Development tools configured and working
- [ ] No functionality lost during restructuring
- [ ] Backup of original structure maintained
- [ ] Clear migration path documented
## Dependencies
- Python 3.8+
- Flask
- SQLAlchemy (for future database integration)
- pytest (for testing)
- flake8, black (for code quality)
## Priority
High - Critical for long-term maintainability
## Estimated Effort
8 days (64 hours)
## Risk Mitigation
- Maintain backup of current structure
- Implement changes incrementally
- Test thoroughly at each phase
- Document rollback procedures
- Keep all API endpoints backward compatible
+97
View File
@@ -0,0 +1,97 @@
# Software Development Team Tasks
## Project: IT Site Survey AI Enhancement
### Current Tasks
#### 1. Database Persistence Implementation
- **Status**: Not Started
- **Priority**: High
- **Owner**: dev-backend
- **Description**: Implement PostgreSQL database persistence
- **Details**: See TASK_DATABASE_PERSISTENCE.md
#### 2. Dashboard Authentication
- **Status**: Not Started
- **Priority**: Medium
- **Owner**: dev-backend + dev-frontend
- **Description**: Add authentication layer to dashboard
- **Details**: Implement user login system to protect survey data
#### 3. Dashboard Enhancement
- **Status**: Not Started
- **Priority**: Medium
- **Owner**: dev-frontend
- **Description**: Add filtering, search, and analytics capabilities
- **Details**: Improve dashboard usability and data analysis features
#### 4. API Documentation
- **Status**: Not Started
- **Priority**: Low
- **Owner**: dev-architect
- **Description**: Create comprehensive API documentation
- **Details**: Document all endpoints, parameters, and responses
### Server-Wide Tasks
#### 5. Server Applications Database Migration
- **Status**: Not Started
- **Priority**: High
- **Owner**: dev-backend
- **Description**: Migrate all server applications to database persistence
- **Details**: IT Site Survey AI, Client Onboarding, and Projects Manager all use in-memory storage
#### 6. Server Applications Authentication
- **Status**: Not Started
- **Priority**: High
- **Owner**: dev-backend + dev-frontend
- **Description**: Add authentication to all application dashboards
- **Details**: Secure access to all three applications
#### 7. Server Applications Structure Improvement
- **Status**: Not Started
- **Priority**: High
- **Owner**: dev-architect + dev-backend + dev-frontend
- **Description**: Restructure all applications to professional standards
- **Details**: See PROJECT_STRUCTURE_IMPROVEMENTS.md for detailed plan
#### 8. Server Applications Testing Implementation
- **Status**: Not Started
- **Priority**: Medium
- **Owner**: dev-qa
- **Description**: Add comprehensive test suites to all applications
- **Details**: Unit tests, integration tests, and end-to-end tests
#### 9. Server Applications CI/CD Setup
- **Status**: Not Started
- **Priority**: Medium
- **Owner**: dev-devops
- **Description**: Implement continuous integration and deployment
- **Details**: Automated testing, deployment pipelines, monitoring
#### 10. Server Applications Monitoring
- **Status**: Not Started
- **Priority**: Medium
- **Owner**: dev-devops
- **Description**: Add health checks and monitoring
- **Details**: Implement logging and status monitoring for all applications
### Completed Tasks
- [x] API endpoint for survey responses (/api/surveys/responses)
- [x] Dashboard interface implementation
- [x] Navigation between main survey and dashboard
- [x] Memory system implementation
### Task Workflow
1. **dev-product** creates detailed task specifications
2. **dev-architect** reviews technical approach
3. **dev-backend** / **dev-frontend** implement functionality
4. **dev-qa** tests implementation
5. **dev-lead** reviews and approves
6. **dev-devops** deploys to production
### Communication
- All task discussions should be documented in task-specific files
- Status updates in this file
- Code reviews via Git pull requests
- Questions to dev-lead (me)
+789
View File
@@ -0,0 +1,789 @@
#!/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 and quote generation.
"""
import os
import json
import uuid
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, ListFlowable, ListItem
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
import io
app = Flask(__name__, static_url_path='', static_folder='.')
CORS(app)
OLLAMA_BASE = os.environ.get('OLLAMA_URL', 'http://192.168.19.25:11434')
# In-memory storage for surveys
surveys_db = {}
survey_responses_db = {}
# Default survey template
DEFAULT_SURVEY_TEMPLATE = {
"id": "concise-network-survey-template",
"name": "Concise Network Site Survey",
"description": "Streamlined one-page network infrastructure survey with dropdown selections",
"questions": [
{
"id": "company_name",
"category": "Client Information",
"question": "Company Name",
"type": "text"
},
{
"id": "industry",
"category": "Client Information",
"question": "Industry/Vertical",
"type": "select",
"options": [
"Healthcare",
"Education",
"Financial Services",
"Manufacturing",
"Retail",
"Technology",
"Government",
"Hospitality",
"Other"
]
},
{
"id": "site_size",
"category": "Site Information",
"question": "Site Size",
"type": "select",
"options": [
"Small (1-10 employees)",
"Medium (11-50 employees)",
"Large (51-250 employees)",
"Enterprise (250+ employees)"
]
},
{
"id": "building_type",
"category": "Site Information",
"question": "Building Type",
"type": "select",
"options": [
"Single Office",
"Multi-tenant",
"Campus",
"Warehouse",
"Retail",
"Industrial",
"Data Center",
"Other"
]
},
{
"id": "square_footage",
"category": "Site Information",
"question": "Total Square Footage",
"type": "select",
"options": [
"Under 5,000 sq ft",
"5,000 - 10,000 sq ft",
"10,000 - 25,000 sq ft",
"25,000 - 50,000 sq ft",
"Over 50,000 sq ft"
]
},
{
"id": "network_topology",
"category": "Existing Infrastructure",
"question": "Current Network Topology",
"type": "select",
"options": [
"None - New Installation",
"Flat Network",
"Two-tier (Access/Distribution)",
"Three-tier (Access/Distribution/Core)",
"Spine-Leaf",
"Other/Not Sure"
]
},
{
"id": "network_size",
"category": "Existing Infrastructure",
"question": "Current Network Size",
"type": "select",
"options": [
"Small (<50 devices)",
"Medium (50-250 devices)",
"Large (250-1000 devices)",
"Enterprise (1000+ devices)"
]
},
{
"id": "internet_bandwidth",
"category": "Connectivity",
"question": "Current Internet Bandwidth",
"type": "select",
"options": [
"Under 50 Mbps",
"50-100 Mbps",
"100-500 Mbps",
"500 Mbps-1 Gbps",
"1-10 Gbps",
"Over 10 Gbps",
"Not Sure"
]
},
{
"id": "isp_type",
"category": "Connectivity",
"question": "ISP Connection Type",
"type": "multiselect",
"options": [
"Fiber",
"Cable/DSL",
"Fixed Wireless",
"5G/LTE",
"Multiple ISPs",
"Satellite",
"Not Sure"
]
},
{
"id": "wireless_standard",
"category": "Wireless Infrastructure",
"question": "Current Wireless Standard",
"type": "select",
"options": [
"None",
"802.11n (Wi-Fi 4)",
"802.11ac (Wi-Fi 5)",
"802.11ax (Wi-Fi 6)",
"802.11be (Wi-Fi 7)",
"Mixed/Not Sure"
]
},
{
"id": "wireless_coverage",
"category": "Wireless Infrastructure",
"question": "Wireless Coverage Quality",
"type": "select",
"options": [
"Excellent - No dead zones",
"Good - Minor coverage issues",
"Fair - Several dead zones",
"Poor - Major coverage issues",
"No wireless network"
]
},
{
"id": "critical_applications",
"category": "Business Requirements",
"question": "Critical Business Applications",
"type": "multiselect",
"options": [
"Email/Collaboration (Office 365, Google Workspace)",
"VoIP/Video Conferencing",
"Cloud Applications",
"File Sharing/Storage",
"Database Applications",
"Remote Desktop/VDI",
"Video Surveillance",
"Point of Sale (POS)",
"Industrial Systems",
"Other"
]
},
{
"id": "device_types",
"category": "Device Requirements",
"question": "Primary Device Types",
"type": "multiselect",
"options": [
"Desktop Computers",
"Laptops",
"Mobile Devices (Phones/Tablets)",
"IoT Devices",
"Printers/MFPs",
"Servers",
"Video Equipment",
"Specialized Equipment"
]
},
{
"id": "user_density",
"category": "Usage Requirements",
"question": "User Density",
"type": "select",
"options": [
"Low (<10 devices per AP)",
"Medium (10-30 devices per AP)",
"High (30-50 devices per AP)",
"Very High (50+ devices per AP)"
]
},
{
"id": "performance_requirements",
"category": "Performance Requirements",
"question": "Network Performance Requirements",
"type": "multiselect",
"options": [
"High Speed (1+ Gbps)",
"Low Latency (<10ms)",
"High Availability (99.9%+)",
"Guest Network Access",
"BYOD Support",
"Outdoor Coverage",
"No Special Requirements"
]
},
{
"id": "security_requirements",
"category": "Security Requirements",
"question": "Security Requirements",
"type": "multiselect",
"options": [
"Basic Firewall",
"Content Filtering",
"Network Segmentation",
"802.1X Authentication",
"Guest Network Isolation",
"Compliance (HIPAA, PCI, etc.)",
"No Special Security Requirements"
]
},
{
"id": "compliance_needs",
"category": "Compliance Requirements",
"question": "Compliance Requirements",
"type": "select",
"options": [
"None",
"HIPAA (Healthcare)",
"PCI-DSS (Payment Card)",
"SOX (Financial)",
"FERPA (Educational)",
"GDPR (Privacy)",
"Industry Specific",
"Multiple Compliance Requirements"
]
},
{
"id": "budget_range",
"category": "Project Information",
"question": "Estimated Budget Range",
"type": "select",
"options": [
"Under $5,000",
"$5,000 - $15,000",
"$15,000 - $50,000",
"$50,000 - $100,000",
"$100,000+",
"Not Established"
]
},
{
"id": "timeline",
"category": "Project Information",
"question": "Project Timeline",
"type": "select",
"options": [
"ASAP - Emergency",
"Within 2 weeks",
"Within 1 month",
"Within 3 months",
"Within 6 months",
"Flexible/Planning Phase"
]
},
{
"id": "additional_notes",
"category": "Additional Information",
"question": "Additional Notes or Special Requirements",
"type": "textarea",
"placeholder": "Please share any other important details about your network requirements..."
}
]
}
def call_ollama(prompt, model='llama3.2:3b', stream=False):
"""Call Ollama API with the given prompt."""
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)}'
@app.route('/')
def index():
return send_from_directory('.', 'index.html')
@app.route('/<path:path>')
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/surveys/template')
def get_template():
return jsonify({'template': DEFAULT_SURVEY_TEMPLATE})
@app.route('/api/surveys', methods=['GET'])
def get_surveys():
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': []
}
surveys_db[survey_id] = survey
return jsonify({'survey': survey}), 201
@app.route('/api/surveys/<survey_id>', methods=['GET'])
def get_survey(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/<survey_id>/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()
response_id = str(uuid.uuid4())
response = {
'id': response_id,
'survey_id': survey_id,
'submitted_by': data.get('submitted_by', 'Anonymous'),
'responses': data.get('responses', {}),
'submitted_at': datetime.utcnow().isoformat()
}
survey['responses'].append(response)
survey_responses_db[response_id] = response
return jsonify({'response': response}), 201
@app.route('/api/surveys/responses', methods=['GET'])
def get_all_responses():
"""Get all survey responses across all surveys."""
all_responses = list(survey_responses_db.values())
return jsonify({'responses': all_responses})
@app.route('/api/surveys/<survey_id>/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
latest_response = survey['responses'][-1]
responses = latest_response['responses']
context = f"""
IT Infrastructure Site Survey Results:
Client: {survey.get('client_name', 'Unknown')}
Site: {survey.get('site_name', 'Unknown')}
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"
prompt = f"""You are a senior network architect and IT infrastructure consultant. Based on the following site survey, provide a comprehensive network infrastructure recommendation.
{context}
Generate a detailed IT infrastructure recommendation report including:
1. **EXECUTIVE SUMMARY**
- Overview of the site and requirements
- Recommended approach (high-level)
2. **NETWORK TOPOLOGY RECOMMENDATION**
- Recommended network architecture (star, mesh, hybrid)
- VLAN structure if applicable
- Network segmentation strategy
3. **HARDWARE RECOMMENDATIONS**
- Firewall/router specifications
- Switch recommendations (managed/unmanaged, PoE needs)
- Wireless access point placement and quantity
- Cabling requirements
4. **INTERNET CONNECTIVITY**
- Recommended ISP and bandwidth
- Backup connectivity options
- WAN configuration if multi-site
5. **SECURITY RECOMMENDATIONS**
- Security appliances/services needed
- Compliance considerations
- Access control policies
6. **ESTIMATED COSTS**
- Equipment costs
- Installation costs
- Ongoing service costs
7. **IMPLEMENTATION TIMELINE**
- Phase 1: Planning and procurement
- Phase 2: Installation
- Phase 3: Testing and cutover
8. **PRIORITY ACTIONS**
- 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': 'llama3.2:3b', '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/<survey_id>/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/<survey_id>/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))
# 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"<b>{q['category']}</b>", 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/<survey_id>/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']
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')}
{'=' * 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/<survey_id>/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"""
<h2>IT Site Survey Report - {survey.get('client_name', 'Client')}</h2>
<p><strong>Site:</strong> {survey.get('site_name', 'Unknown')}</p>
<p><strong>Date:</strong> {datetime.now().strftime('%B %d, %Y')}</p>
<hr>
<h3>Survey Responses:</h3>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
<tr style="background-color: #f0f0f0;">
<th>Category</th>
<th>Question</th>
<th>Answer</th>
</tr>
"""
for q in survey['questions']:
qid = q['id']
answer = responses.get(qid, 'Not answered')
if isinstance(answer, list):
answer = ', '.join(answer)
email_body += f"""
<tr>
<td>{q['category']}</td>
<td>{q['question']}</td>
<td>{answer}</td>
</tr>
"""
email_body += """
</table>
<p>---<br>
Report generated by IT Site Survey AI<br>
""" + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "</p>
"""
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("───────────────────────────────────────")
app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
+416
View File
@@ -0,0 +1,416 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Site Survey AI | Dashboard</title>
<style>
:root {
--bg: #0d1117;
--bg-2: #161b22;
--bg-3: #21262d;
--border: #30363d;
--text: #c9d1d9;
--text-2: #8b949e;
--accent: #58a6ff;
--accent-2: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.logo {
font-size: 3rem;
margin-bottom: 10px;
}
h1 {
font-size: 2rem;
font-weight: 600;
margin-bottom: 5px;
}
.subtitle {
color: var(--text-2);
font-size: 1.1rem;
}
.panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 25px;
margin-bottom: 20px;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.panel-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
text-decoration: none;
font-size: 0.9rem;
}
.btn-primary {
background: var(--accent-2);
color: white;
}
.btn-primary:hover {
background: var(--accent);
}
.btn-secondary {
background: var(--bg-3);
color: var(--text);
border-color: var(--border);
text-decoration: none;
}
.btn-secondary:hover {
background: var(--bg-3);
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert.info {
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.3);
}
.alert.error {
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
}
.hidden {
display: none !important;
}
.response-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 15px;
}
.response-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.response-meta {
font-size: 0.9rem;
color: var(--text-2);
}
.response-answers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.answer-item {
margin-bottom: 10px;
}
.answer-label {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-2);
margin-bottom: 3px;
}
.answer-value {
font-size: 0.95rem;
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: var(--text-2);
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.response-answers {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">🏢</div>
<h1>IT Site Survey AI</h1>
<div class="subtitle">Network Infrastructure Planning & Recommendations</div>
</header>
<div class="panel">
<div class="panel-header">
<h2>📊 Survey Dashboard</h2>
<a href="/" class="btn btn-secondary">← Back to Survey</a>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalSurveys">0</div>
<div class="stat-label">Total Surveys</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalResponses">0</div>
<div class="stat-label">Total Responses</div>
</div>
<div class="stat-card">
<div class="stat-value" id="todayResponses">0</div>
<div class="stat-label">Today's Responses</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgCompletion">0%</div>
<div class="stat-label">Avg. Completion</div>
</div>
</div>
<div id="loadingMessage" class="alert info">
Loading survey responses...
</div>
<div id="errorMessage" class="alert error hidden">
Error loading survey responses. Please try again.
</div>
<div id="responsesContainer">
<!-- Responses will be loaded here -->
</div>
</div>
</div>
<script>
// Format date for display
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString();
}
// Format response value for display
function formatResponseValue(value) {
if (Array.isArray(value)) {
return value.join(', ');
}
return value || 'Not answered';
}
// Load all survey responses
async function loadResponses() {
const loadingMessage = document.getElementById('loadingMessage');
const errorMessage = document.getElementById('errorMessage');
const responsesContainer = document.getElementById('responsesContainer');
try {
// Fetch all responses
const response = await fetch('/api/surveys/responses');
if (!response.ok) {
throw new Error('Failed to load responses');
}
const data = await response.json();
const responses = data.responses || [];
// Update stats
updateStats(responses);
// Hide loading message
loadingMessage.classList.add('hidden');
// Display responses
if (responses.length === 0) {
responsesContainer.innerHTML = '<div class="alert info">No survey responses yet. Complete a survey to see results here.</div>';
return;
}
// Sort responses by submission date (newest first)
responses.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at));
// Generate HTML for responses
let html = '';
for (const response of responses) {
html += `
<div class="response-card">
<div class="response-header">
<div>
<strong>Response #${response.id.substring(0, 8)}</strong>
</div>
<div class="response-meta">
Submitted: ${formatDate(response.submitted_at)} by ${response.submitted_by || 'Anonymous'}
</div>
</div>
<div class="response-answers">
${generateAnswersHtml(response.responses)}
</div>
<div class="actions">
<button class="btn btn-primary" onclick="viewSurvey('${response.survey_id}')">View Survey</button>
<button class="btn btn-secondary" onclick="analyzeResponse('${response.survey_id}')">Analyze</button>
<button class="btn btn-secondary" onclick="generateQuote('${response.survey_id}')">Generate Quote</button>
</div>
</div>
`;
}
responsesContainer.innerHTML = html;
} catch (error) {
console.error('Error loading responses:', error);
loadingMessage.classList.add('hidden');
errorMessage.classList.remove('hidden');
}
}
// Generate HTML for answers
function generateAnswersHtml(responses) {
let html = '';
// Define the order of questions based on the template
const questionOrder = [
'company_name', 'industry', 'site_size', 'building_type', 'square_footage',
'network_topology', 'network_size', 'internet_bandwidth', 'isp_type',
'wireless_standard', 'wireless_coverage', 'critical_applications',
'device_types', 'user_density', 'performance_requirements',
'security_requirements', 'compliance_needs', 'budget_range',
'timeline', 'additional_notes'
];
// Create a map of responses for easier access
const responseMap = responses || {};
// Generate HTML for each question in order
for (const questionId of questionOrder) {
if (responseMap.hasOwnProperty(questionId)) {
const value = responseMap[questionId];
const label = getQuestionLabel(questionId);
html += `
<div class="answer-item">
<div class="answer-label">${label}</div>
<div class="answer-value">${formatResponseValue(value)}</div>
</div>
`;
}
}
return html;
}
// Get human-readable label for question ID
function getQuestionLabel(questionId) {
const labels = {
'company_name': 'Company Name',
'industry': 'Industry',
'site_size': 'Site Size',
'building_type': 'Building Type',
'square_footage': 'Square Footage',
'network_topology': 'Network Topology',
'network_size': 'Network Size',
'internet_bandwidth': 'Internet Bandwidth',
'isp_type': 'ISP Type',
'wireless_standard': 'Wireless Standard',
'wireless_coverage': 'Wireless Coverage',
'critical_applications': 'Critical Applications',
'device_types': 'Device Types',
'user_density': 'User Density',
'performance_requirements': 'Performance Requirements',
'security_requirements': 'Security Requirements',
'compliance_needs': 'Compliance Needs',
'budget_range': 'Budget Range',
'timeline': 'Project Timeline',
'additional_notes': 'Additional Notes'
};
return labels[questionId] || questionId;
}
// Update statistics
function updateStats(responses) {
document.getElementById('totalResponses').textContent = responses.length;
// For now, we'll set other stats to default values
// In a more complete implementation, we would calculate these from the data
document.getElementById('totalSurveys').textContent = responses.length > 0 ? '1' : '0';
document.getElementById('todayResponses').textContent = '0';
document.getElementById('avgCompletion').textContent = '100%';
}
// View survey details
function viewSurvey(surveyId) {
window.location.href = `/?survey=${surveyId}`;
}
// Analyze response
function analyzeResponse(surveyId) {
// In a full implementation, this would trigger the AI analysis
alert('AI analysis would be triggered here for survey: ' + surveyId);
}
// Generate quote
function generateQuote(surveyId) {
// In a full implementation, this would generate a quote
alert('Quote generation would be triggered here for survey: ' + surveyId);
}
// Load responses when page loads
document.addEventListener('DOMContentLoaded', function() {
loadResponses();
});
</script>
</body>
</html>
+520
View File
@@ -0,0 +1,520 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Site Survey AI | Network Infrastructure Planning</title>
<style>
:root {
--bg: #0d1117;
--bg-2: #161b22;
--bg-3: #21262d;
--border: #30363d;
--text: #c9d1d9;
--text-2: #8b949e;
--accent: #58a6ff;
--accent-2: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.logo {
font-size: 3rem;
margin-bottom: 10px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 5px;
}
.subtitle {
color: var(--text-2);
font-size: 1rem;
}
.panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 25px;
margin-bottom: 20px;
}
.panel h2 {
font-size: 1.1rem;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 12px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: inherit;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.form-group select {
cursor: pointer;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input {
width: auto;
}
.question-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 15px;
}
.question-card .category {
font-size: 0.75rem;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.question-card .question-text {
font-size: 1rem;
margin-bottom: 12px;
}
.btn {
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius);
padding: 12px 24px;
font-family: inherit;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn:hover {
background: var(--accent-2);
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(88,166,255,0.3);
}
.btn:disabled {
background: var(--bg-3) !important;
color: var(--text-2) !important;
cursor: not-allowed;
transform: none !important;
}
.btn-success {
background: var(--success);
}
.btn-success:hover {
background: #2ea043;
box-shadow: 0 4px 15px rgba(63,185,80,0.3);
}
.btn-secondary {
background: var(--bg-3);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--bg-3);
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.alert {
padding: 15px;
border-radius: var(--radius);
margin-bottom: 20px;
}
.alert.info {
background: rgba(88,166,255,0.1);
border: 1px solid var(--accent);
color: var(--accent);
}
.alert.success {
background: rgba(63,185,80,0.1);
border: 1px solid var(--success);
color: var(--success);
}
.alert.error {
background: rgba(248,81,73,0.1);
border: 1px solid var(--danger);
color: var(--danger);
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
.photo-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.photo-preview {
position: relative;
border-radius: var(--radius);
overflow: hidden;
height: 120px;
}
.photo-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-preview .remove-photo {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.7);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 12px;
}
.photo-upload-area {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 30px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.photo-upload-area:hover {
border-color: var(--accent);
}
.photo-upload-area input {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">🏢</div>
<h1>IT Site Survey AI</h1>
<div class="subtitle">Network Infrastructure Planning & Recommendations</div>
<div style="margin-top: 15px;">
<a href="/dashboard.html" class="btn btn-secondary">📊 Dashboard</a>
</div>
</header>
<div class="panel">
<h2>📋 Network Site Survey</h2>
<p style="color: var(--text-2); margin-bottom: 20px;">Complete this concise survey to generate AI-powered network infrastructure recommendations.</p>
<div id="surveyForm">
<div id="surveyQuestions">
<div class="alert info">Loading survey questions...</div>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border);">
<h3 style="font-size: 1rem; margin-bottom: 15px;">📷 Site Photos (Optional)</h3>
<div class="photo-preview-grid" id="photoPreviewGrid"></div>
<div class="photo-upload-area" onclick="document.getElementById("photoInput").click()">
<input type="file" id="photoInput" accept="image/*" multiple onchange="handlePhotoSelect(this.files)">
<div style="font-size: 2rem; margin-bottom: 10px;">📷</div>
<div><strong>Click to upload</strong> or drag and drop<br>
<small style="color: var(--text-2);">JPG, PNG, WebP • Multiple files allowed</small></div>
</div>
</div>
<div class="form-group" style="margin-top: 20px;">
<label>Your Name</label>
<input type="text" id="submitterName" placeholder="Enter your name">
</div>
<div id="errorMessage" class="alert error hidden"></div>
<div id="successMessage" class="alert success hidden">
Survey submitted successfully! You can now view AI recommendations.
</div>
<div class="actions">
<button class="btn btn-success" onclick="submitSurvey()" id="submitBtn">
<span id="submitSpinner" class="spinner hidden"></span>
Submit Survey
</button>
</div>
</div>
</div>
</div>
<script>
let surveyTemplate = null;
let surveyResponses = {};
let selectedPhotos = [];
// Initialize
document.addEventListener("DOMContentLoaded", function() {
loadTemplate();
});
async function loadTemplate() {
try {
const response = await fetch("/api/surveys/template");
const data = await response.json();
surveyTemplate = data.template;
renderQuestions(surveyTemplate.questions);
} catch (e) {
console.error("Failed to load survey template:", e);
document.getElementById("surveyQuestions").innerHTML = "<div class=\"alert error\">Failed to load survey questions. Please refresh the page.</div>";
}
}
function renderQuestions(questions) {
const container = document.getElementById("surveyQuestions");
surveyResponses = {};
container.innerHTML = questions.map(q => {
let inputHtml = "";
switch(q.type) {
case "text":
inputHtml = "<input type=\"text\" id=\"q_" + q.id + "\" placeholder=\"" + (q.placeholder || "") + "\" onchange=\"saveResponse( + q.id + , this.value)\">";
break;
case "number":
inputHtml = "<input type=\"number\" id=\"q_" + q.id + "\" placeholder=\"" + (q.placeholder || "") + "\" onchange=\"saveResponse( + q.id + , this.value)\">";
break;
case "select":
inputHtml = "<select id=\"q_" + q.id + "\" onchange=\"saveResponse( + q.id + , this.value)\">" +
"<option value=\"\">Select...</option>";
if (q.options) {
q.options.forEach(opt => {
inputHtml += "<option value=\"" + opt + "\">" + opt + "</option>";
});
}
inputHtml += "</select>";
break;
case "multiselect":
inputHtml = "<div class=\"checkbox-group\">";
if (q.options) {
q.options.forEach(opt => {
inputHtml += "<label class=\"checkbox-item\">" +
"<input type=\"checkbox\" value=\"" + opt + "\" onchange=\"saveMultiResponse( + q.id + , + opt + , this.checked)\">" +
opt +
"</label>";
});
}
inputHtml += "</div>";
break;
case "textarea":
inputHtml = "<textarea id=\"q_" + q.id + "\" placeholder=\"" + (q.placeholder || "") + "\" onchange=\"saveResponse( + q.id + , this.value)\"></textarea>";
break;
}
return "<div class=\"question-card\">" +
"<div class=\"category\">" + q.category + "</div>" +
"<div class=\"question-text\">" + q.question + "</div>" +
inputHtml +
"</div>";
}).join("");
}
function saveResponse(questionId, value) {
surveyResponses[questionId] = value;
}
function saveMultiResponse(questionId, optionValue, isChecked) {
if (!surveyResponses[questionId]) {
surveyResponses[questionId] = [];
}
if (isChecked) {
surveyResponses[questionId].push(optionValue);
} else {
surveyResponses[questionId] = surveyResponses[questionId].filter(v => v !== optionValue);
}
}
function handlePhotoSelect(files) {
for (const file of files) {
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
selectedPhotos.push({
file: file,
preview: e.target.result
});
renderPhotoPreviews();
};
reader.readAsDataURL(file);
}
}
}
function removePhoto(index) {
selectedPhotos.splice(index, 1);
renderPhotoPreviews();
}
function renderPhotoPreviews() {
const container = document.getElementById("photoPreviewGrid");
container.innerHTML = selectedPhotos.map((photo, index) => {
return "<div class=\"photo-preview\">" +
"<img src=\"" + photo.preview + "\" alt=\"Preview\">" +
"<button class=\"remove-photo\" onclick=\"removePhoto(" + index + ")\">×</button>" +
"</div>";
}).join("");
}
function showError(message) {
const errorEl = document.getElementById("errorMessage");
errorEl.textContent = message;
errorEl.classList.remove("hidden");
// Hide success message if shown
document.getElementById("successMessage").classList.add("hidden");
}
function hideError() {
document.getElementById("errorMessage").classList.add("hidden");
}
function showSuccess() {
document.getElementById("successMessage").classList.remove("hidden");
// Hide error message if shown
document.getElementById("errorMessage").classList.add("hidden");
}
async function submitSurvey() {
const btn = document.getElementById("submitBtn");
const spinner = document.getElementById("submitSpinner");
const submitterName = document.getElementById("submitterName").value.trim();
// Validate input
if (!submitterName) {
showError("Please enter your name");
return;
}
// Disable button and show spinner
btn.disabled = true;
spinner.classList.remove("hidden");
hideError();
try {
// Create survey first with default values
const createResponse = await fetch("/api/surveys", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Network Site Survey",
client_name: "Client",
site_name: "Site",
description: "One-page survey submission"
})
});
if (!createResponse.ok) {
const errorText = await createResponse.text();
throw new Error("Failed to create survey: " + createResponse.status + " - " + errorText);
}
const createData = await createResponse.json();
const surveyId = createData.survey.id;
// Submit responses with submitter name
const response = await fetch("/api/surveys/" + surveyId + "/responses", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
submitter_name: submitterName,
responses: surveyResponses
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error("Failed to submit responses: " + response.status + " - " + errorText);
}
// Upload photos if any
if (selectedPhotos.length > 0) {
for (const photo of selectedPhotos) {
const formData = new FormData();
formData.append("photo", photo.file);
await fetch("/api/surveys/" + surveyId + "/photos", {
method: "POST",
body: formData
});
}
}
console.log("Survey submitted successfully!");
showSuccess();
btn.textContent = "Submitted!";
} catch (error) {
console.error("Error submitting survey:", error);
showError("Error submitting survey: " + error.message);
btn.disabled = false;
spinner.classList.add("hidden");
btn.textContent = "Submit Survey";
}
}
</script>
</body>
</html>