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:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user