Merges ac-helper (PHP Activation Calendar) and brief-extractor (Python AI) into a single Docker app with React/TypeScript frontend. Features: - Brief upload → AI extraction → review → Activation Calendar import - Handsontable v17 spreadsheet with dependent dropdowns (148 categories) - AI natural language commands via Gemini (YOLO mode, voice input) - Azure AD MSAL SPA PKCE authentication, user roles (user/admin) - CSV Activation Calendar export - Real-time WebSocket job progress - Admin: user management, dropdown Excel upload - Multi-stage Dockerfile, docker-compose, nginx proxy instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
No EOL
3.9 KiB
Python
Executable file
123 lines
No EOL
3.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Startup script for Brief Extractor GUI server
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
# Add server and core paths to Python path
|
|
project_root = Path(__file__).parent
|
|
sys.path.insert(0, str(project_root))
|
|
sys.path.insert(0, str(project_root / 'server'))
|
|
|
|
# Set up logging before importing modules
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
async def async_main():
|
|
"""Async main function with proper signal handling"""
|
|
import asyncio
|
|
import signal
|
|
|
|
# Import after path setup
|
|
from server.app import create_app
|
|
from server.config_runtime import server_config
|
|
|
|
# Validate configuration
|
|
if not server_config.validate_auth_config():
|
|
if not server_config.DEV_MODE:
|
|
logger.error("MSAL authentication configuration is incomplete")
|
|
logger.error("Please set MSAL_CLIENT_ID, MSAL_CLIENT_SECRET, and MSAL_TENANT_ID in .env")
|
|
sys.exit(1)
|
|
else:
|
|
logger.warning("Running in DEV_MODE - MSAL authentication bypassed")
|
|
|
|
# Create application
|
|
logger.info("Creating Brief Extractor GUI application...")
|
|
app = create_app()
|
|
|
|
# Import and configure Hypercorn
|
|
import hypercorn.asyncio
|
|
from hypercorn import Config
|
|
|
|
config = Config()
|
|
config.bind = [f"{server_config.HOST}:{server_config.PORT}"]
|
|
config.workers = server_config.WORKERS
|
|
config.use_reloader = server_config.DEBUG
|
|
config.accesslog = "-" # Log to stdout
|
|
config.errorlog = "-" # Log to stderr
|
|
|
|
# Log startup information
|
|
logger.info(f"Starting Brief Extractor GUI server")
|
|
logger.info(f"Server: http://{server_config.HOST}:{server_config.PORT}")
|
|
logger.info(f"Development mode: {server_config.DEV_MODE}")
|
|
logger.info(f"Max concurrent jobs: {server_config.MAX_CONCURRENT_JOBS}")
|
|
logger.info(f"Max upload size: {server_config.MAX_UPLOAD_SIZE_MB}MB")
|
|
logger.info(f"File retention: {server_config.FILE_RETENTION_HOURS} hours")
|
|
logger.info(f"Workers: {server_config.WORKERS}")
|
|
|
|
# Set up proper signal handling for graceful shutdown
|
|
shutdown_event = asyncio.Event()
|
|
|
|
def signal_handler():
|
|
logger.info("Shutdown signal received, stopping server...")
|
|
shutdown_event.set()
|
|
|
|
# Force shutdown after 3 seconds if graceful shutdown fails
|
|
def force_shutdown():
|
|
import time
|
|
time.sleep(3)
|
|
logger.warning("Graceful shutdown timed out, forcing exit...")
|
|
os._exit(1)
|
|
|
|
import threading
|
|
threading.Thread(target=force_shutdown, daemon=True).start()
|
|
|
|
# Register signal handlers
|
|
if sys.platform != 'win32':
|
|
loop = asyncio.get_running_loop()
|
|
loop.add_signal_handler(signal.SIGINT, signal_handler)
|
|
loop.add_signal_handler(signal.SIGTERM, signal_handler)
|
|
|
|
try:
|
|
# Start server with shutdown trigger
|
|
await hypercorn.asyncio.serve(app, config, shutdown_trigger=shutdown_event.wait)
|
|
logger.info("Server stopped gracefully")
|
|
|
|
except asyncio.CancelledError:
|
|
logger.info("Server cancelled")
|
|
except Exception as e:
|
|
logger.error(f"Server error: {e}", exc_info=True)
|
|
raise
|
|
|
|
def main():
|
|
"""Main entry point"""
|
|
import asyncio
|
|
import signal
|
|
|
|
# Set up immediate signal handling before async loop
|
|
def immediate_shutdown(signum, frame):
|
|
logger.info(f"Immediate shutdown signal {signum} received")
|
|
os._exit(0)
|
|
|
|
signal.signal(signal.SIGINT, immediate_shutdown)
|
|
signal.signal(signal.SIGTERM, immediate_shutdown)
|
|
|
|
try:
|
|
asyncio.run(async_main())
|
|
except KeyboardInterrupt:
|
|
logger.info("Server stopped by user")
|
|
os._exit(0)
|
|
except Exception as e:
|
|
logger.error(f"Server failed to start: {e}", exc_info=True)
|
|
os._exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
main() |