| from flask import Flask, render_template, request, jsonify
|
| from flask_cors import CORS
|
| from datetime import datetime
|
| import os
|
|
|
| app = Flask(__name__)
|
| CORS(app)
|
|
|
|
|
|
|
| current_urls = {
|
| 'base_url': '',
|
| 'last_updated': '',
|
| 'portals': []
|
| }
|
|
|
|
|
| WINDOW_DEFINITIONS = [
|
| {
|
| 'window': 2,
|
| 'name': 'Tech Analysis',
|
| 'path': '/window2',
|
| 'icon': 'cpu',
|
| 'color': 'blue',
|
| 'description': 'TEC model β technology threat scores for any year',
|
| },
|
| {
|
| 'window': 3,
|
| 'name': 'Conductor Synthesis',
|
| 'path': '/window3',
|
| 'icon': 'git-branch',
|
| 'color': 'purple',
|
| 'description': 'Groq synthesis of tech threats into strategic insights',
|
| },
|
| {
|
| 'window': 4,
|
| 'name': 'Economic Analysis',
|
| 'path': '/window4',
|
| 'icon': 'trending-up',
|
| 'color': 'yellow',
|
| 'description': '12 economic impact indicators from conductor results',
|
| },
|
| {
|
| 'window': 5,
|
| 'name': 'War Analysis',
|
| 'path': '/window5',
|
| 'icon': 'shield',
|
| 'color': 'red',
|
| 'description': 'Dual AI + live conflict data β war risk predictions',
|
| },
|
| {
|
| 'window': 6,
|
| 'name': 'Climate Analysis',
|
| 'path': '/window6',
|
| 'icon': 'cloud',
|
| 'color': 'cyan',
|
| 'description': 'World Bank CCKP β climate-conflict nexus assessment',
|
| },
|
| {
|
| 'window': 7,
|
| 'name': 'Disease Prediction',
|
| 'path': '/window7',
|
| 'icon': 'activity',
|
| 'color': 'orange',
|
| 'description': 'AI biosecurity risk β top 3 disease outbreak predictions',
|
| },
|
| {
|
| 'window': 8,
|
| 'name': 'Drug Discovery',
|
| 'path': '/window8',
|
| 'icon': 'zap',
|
| 'color': 'green',
|
| 'description': '4-phase pipeline β SMILES, DiffDock, Vina, manufacturing',
|
| },
|
| {
|
| 'window': 9,
|
| 'name': 'Visual Disease',
|
| 'path': '/window9',
|
| 'icon': 'image',
|
| 'color': 'pink',
|
| 'description': 'AI-generated pathogen morphology visualizations',
|
| },
|
| {
|
| 'window': 10,
|
| 'name': 'Visual Kinetic',
|
| 'path': '/window10',
|
| 'icon': 'eye',
|
| 'color': 'indigo',
|
| 'description': '3D PK/PD simulation with live cardiac monitoring',
|
| },
|
| {
|
| 'window': 11,
|
| 'name': 'AR Portal',
|
| 'path': '/window11',
|
| 'icon': 'scan',
|
| 'color': 'teal',
|
| 'description': 'Augmented Reality β scan QR to view all windows remotely',
|
| },
|
| ]
|
|
|
|
|
| def build_portals(base_url: str) -> list:
|
| """Build portal list from window definitions + a given base URL."""
|
| return [
|
| {
|
| **w,
|
| 'url': f"{base_url.rstrip('/')}{w['path']}",
|
| 'qr_url': f"{base_url.rstrip('/')}{w['path']}",
|
| }
|
| for w in WINDOW_DEFINITIONS
|
| ]
|
|
|
|
|
|
|
|
|
| @app.route('/')
|
| def index():
|
| """AR Portal Dashboard β shows QR grid for all windows."""
|
| return render_template(
|
| 'ar_dashboard.html',
|
| base_url=current_urls.get('base_url', ''),
|
| last_updated=current_urls.get('last_updated', ''),
|
| portals=current_urls.get('portals', []),
|
| )
|
|
|
|
|
| @app.route('/ping', methods=['GET'])
|
| def ping():
|
| """UptimeRobot keep-alive β returns 200 instantly, no heavy work."""
|
| return jsonify({'status': 'ok', 'message': 'AR Space is alive'}), 200
|
|
|
|
|
| @app.route('/health')
|
| def health():
|
| """Health check."""
|
| return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
|
|
|
|
|
| @app.route('/api/status')
|
| def status():
|
| """Current AR system status β called by the worker."""
|
| return jsonify({
|
| 'status': 'online',
|
| 'base_url': current_urls.get('base_url', ''),
|
| 'last_updated': current_urls.get('last_updated', ''),
|
| 'portals_count': len(current_urls.get('portals', [])),
|
| 'total_windows': len(WINDOW_DEFINITIONS),
|
| })
|
|
|
|
|
| @app.route('/api/update-urls', methods=['POST'])
|
| def update_urls():
|
| """
|
| Called by the Cloudflare Worker (or directly) to register the frontend
|
| base URL. Once set, /api/qr-data returns ready-to-use QR links.
|
| """
|
| try:
|
| data = request.get_json()
|
| if not data or 'base_url' not in data:
|
| return jsonify({'error': 'base_url is required'}), 400
|
|
|
| base_url = data['base_url'].rstrip('/')
|
| portals = build_portals(base_url)
|
| timestamp = datetime.now().isoformat()
|
|
|
| current_urls.update({
|
| 'base_url': base_url,
|
| 'last_updated': timestamp,
|
| 'portals': portals,
|
| })
|
|
|
| return jsonify({
|
| 'success': True,
|
| 'message': 'URLs updated successfully',
|
| 'base_url': base_url,
|
| 'timestamp': timestamp,
|
| 'portals_count': len(portals),
|
| })
|
|
|
| except Exception as e:
|
| return jsonify({'error': str(e)}), 500
|
|
|
|
|
| @app.route('/api/qr-data', methods=['GET'])
|
| def qr_data():
|
| """
|
| Returns all window URLs ready for QR-code generation.
|
| Window 11 frontend calls this via the worker proxy to render the QR grid.
|
| Optional ?window=N to get a single window's data.
|
| """
|
| window_param = request.args.get('window')
|
| base_url = current_urls.get('base_url', '')
|
|
|
| if not base_url:
|
| return jsonify({
|
| 'error': 'base_url not configured yet',
|
| 'hint': 'POST {"base_url": "https://your-app.com"} to /api/update-urls',
|
| 'portals': [],
|
| }), 503
|
|
|
| portals = current_urls.get('portals') or build_portals(base_url)
|
|
|
| if window_param:
|
| try:
|
| wnum = int(window_param)
|
| portal = next((p for p in portals if p['window'] == wnum), None)
|
| if not portal:
|
| return jsonify({'error': f'Window {wnum} not found'}), 404
|
| return jsonify({'success': True, 'portal': portal})
|
| except ValueError:
|
| return jsonify({'error': 'window must be an integer'}), 400
|
|
|
| return jsonify({
|
| 'success': True,
|
| 'base_url': base_url,
|
| 'last_updated': current_urls.get('last_updated', ''),
|
| 'portals': portals,
|
| 'total': len(portals),
|
| })
|
|
|
|
|
| @app.route('/api/windows', methods=['GET'])
|
| def windows():
|
| """Static list of all window definitions (no base_url needed)."""
|
| return jsonify({
|
| 'success': True,
|
| 'windows': WINDOW_DEFINITIONS,
|
| 'total': len(WINDOW_DEFINITIONS),
|
| })
|
|
|
|
|
| if __name__ == '__main__':
|
| port = int(os.environ.get('PORT', 7860))
|
| app.run(host='0.0.0.0', port=port, debug=False)
|
|
|