Add initial PWA application structure
	
		
			
	
		
	
	
		
			
				
	
				ci/woodpecker/push/woodpecker Pipeline was successful
				
					Details
				
			
		
	
				
					
				
			
				
	
				ci/woodpecker/push/woodpecker Pipeline was successful
				
					Details
				
			
		
	This commit is contained in:
		
							parent
							
								
									05eb81aa64
								
							
						
					
					
						commit
						4a0797fcff
					
				|  | @ -0,0 +1,42 @@ | |||
| # Python | ||||
| __pycache__/ | ||||
| *.py[cod] | ||||
| *$py.class | ||||
| *.so | ||||
| .Python | ||||
| env/ | ||||
| build/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| downloads/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
| 
 | ||||
| # Virtual environments | ||||
| venv/ | ||||
| ENV/ | ||||
| .env | ||||
| 
 | ||||
| # IDE specific files | ||||
| .idea/ | ||||
| .vscode/ | ||||
| *.swp | ||||
| *.swo | ||||
| 
 | ||||
| # Docker | ||||
| .dockerignore | ||||
| 
 | ||||
| # Application specific | ||||
| docker/midtownplaydio/src/static/ | ||||
| 
 | ||||
| # OS specific | ||||
| .DS_Store | ||||
| Thumbs.db  | ||||
|  | @ -0,0 +1,56 @@ | |||
| # MidTownPlaydio | ||||
| 
 | ||||
| A progressive web app (PWA) to stream Midtown Radio KW in a mobile-friendly interface. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| - Responsive web interface | ||||
| - Progressive Web App (PWA) capabilities | ||||
| - Offline caching | ||||
| - Mobile-friendly design | ||||
| 
 | ||||
| ## Development | ||||
| 
 | ||||
| ### Prerequisites | ||||
| 
 | ||||
| - Docker and Docker Compose | ||||
| 
 | ||||
| ### Running Locally | ||||
| 
 | ||||
| ```bash | ||||
| # Clone the repository | ||||
| git clone https://git.nixc.us/colin/midtownplaydio.git | ||||
| cd midtownplaydio | ||||
| 
 | ||||
| # Start the development server | ||||
| docker-compose up | ||||
| ``` | ||||
| 
 | ||||
| The app will be available at http://localhost:3000 | ||||
| 
 | ||||
| ### Directory Structure | ||||
| 
 | ||||
| ``` | ||||
| ├── docker/ | ||||
| │   └── midtownplaydio/ | ||||
| │       ├── src/             # Source code | ||||
| │       ├── Dockerfile       # Development/staging Dockerfile | ||||
| │       └── Dockerfile.production  # Production Dockerfile | ||||
| ├── docker-compose.yml       # Local development setup | ||||
| ├── docker-compose.staging.yml  # Staging deployment | ||||
| ├── docker-compose.production.yml  # Production deployment | ||||
| ├── stack.staging.yml        # Docker Swarm stack for staging | ||||
| ├── stack.production.yml     # Docker Swarm stack for production | ||||
| └── .woodpecker.yml          # CI/CD configuration | ||||
| ``` | ||||
| 
 | ||||
| ## Deployment | ||||
| 
 | ||||
| The application is set up for automatic deployment through Woodpecker CI. | ||||
| 
 | ||||
| - Push to `main` branch deploys to staging environment | ||||
| - Cron job or manual promotion deploys to production | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| Copyright (c) 2023  | ||||
|  | @ -0,0 +1,14 @@ | |||
| services: | ||||
|   fancy-qr: | ||||
|     build: | ||||
|       context: ./docker/fancy-qr | ||||
|       dockerfile: Dockerfile | ||||
|     ports: | ||||
|       - "3000:3000" | ||||
|     volumes: | ||||
|       - ./docker/fancy-qr/src:/app/web | ||||
|       - ./uploads:/app/web/uploads | ||||
|       - ./output:/app/web/outputs | ||||
|     environment: | ||||
|       - NODE_ENV=development | ||||
|     restart: unless-stopped  | ||||
|  | @ -0,0 +1,31 @@ | |||
| FROM python:3.11-slim | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| # Copy requirements first for better caching | ||||
| COPY src/requirements.txt . | ||||
| RUN pip install --no-cache-dir -r requirements.txt | ||||
| 
 | ||||
| # Install development tools | ||||
| RUN pip install --no-cache-dir pytest pytest-cov black isort | ||||
| 
 | ||||
| # Copy the rest of the application | ||||
| COPY src/ . | ||||
| 
 | ||||
| # Create directory for static files | ||||
| RUN mkdir -p /app/static | ||||
| 
 | ||||
| # Create non-root user for security | ||||
| RUN adduser --disabled-password --gecos '' appuser | ||||
| RUN chown -R appuser:appuser /app | ||||
| USER appuser | ||||
| 
 | ||||
| # Expose the port the app runs on | ||||
| EXPOSE 3000 | ||||
| 
 | ||||
| # Health check | ||||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ | ||||
|   CMD curl -f http://localhost:3000/health || exit 1 | ||||
| 
 | ||||
| # Start with uvicorn for hot reloading | ||||
| CMD uvicorn app:app --host 0.0.0.0 --port 3000 --reload  | ||||
|  | @ -0,0 +1,28 @@ | |||
| FROM python:3.11-slim | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| # Copy requirements first for better caching | ||||
| COPY src/requirements.txt . | ||||
| RUN pip install --no-cache-dir -r requirements.txt | ||||
| 
 | ||||
| # Copy the rest of the application | ||||
| COPY src/ . | ||||
| 
 | ||||
| # Create directory for static files | ||||
| RUN mkdir -p /app/static | ||||
| 
 | ||||
| # Create non-root user for security | ||||
| RUN adduser --disabled-password --gecos '' appuser | ||||
| RUN chown -R appuser:appuser /app | ||||
| USER appuser | ||||
| 
 | ||||
| # Expose the port the app runs on | ||||
| EXPOSE 3000 | ||||
| 
 | ||||
| # Health check | ||||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ | ||||
|   CMD curl -f http://localhost:3000/health || exit 1 | ||||
| 
 | ||||
| # Run the application with Gunicorn | ||||
| CMD gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:3000 app:app | ||||
|  | @ -0,0 +1,161 @@ | |||
| from fastapi import FastAPI, Request | ||||
| from fastapi.responses import HTMLResponse, JSONResponse, FileResponse | ||||
| from fastapi.staticfiles import StaticFiles | ||||
| import os | ||||
| import json | ||||
| 
 | ||||
| app = FastAPI(title="MidTownPlaydio") | ||||
| 
 | ||||
| # Directory to store static files | ||||
| STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") | ||||
| os.makedirs(STATIC_DIR, exist_ok=True) | ||||
| 
 | ||||
| # Generate index.html | ||||
| def generate_index_html(): | ||||
|     with open(os.path.join(STATIC_DIR, 'index.html'), 'w') as f: | ||||
|         f.write(''' | ||||
|         <!DOCTYPE html> | ||||
|         <html> | ||||
|         <head> | ||||
|             <title>MidTownPlaydio</title> | ||||
|             <meta charset="UTF-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|             <link rel="manifest" href="/manifest.json"> | ||||
|             <meta name="theme-color" content="#ffffff"> | ||||
|             <style> | ||||
|                 body { | ||||
|                     font-family: Arial, sans-serif; | ||||
|                     margin: 0; | ||||
|                     padding: 0; | ||||
|                     display: flex; | ||||
|                     flex-direction: column; | ||||
|                     align-items: center; | ||||
|                     justify-content: center; | ||||
|                     height: 100vh; | ||||
|                     background-color: #f0f0f0; | ||||
|                 } | ||||
|                 .container { | ||||
|                     text-align: center; | ||||
|                     padding: 20px; | ||||
|                 } | ||||
|                 h1 { | ||||
|                     color: #333; | ||||
|                 } | ||||
|             </style> | ||||
|         </head> | ||||
|         <body> | ||||
|             <div class="container"> | ||||
|                 <h1>MidTownPlaydio</h1> | ||||
|                 <iframe src="https://midtownradiokw.airtime.pro/player" width="300" height="200" allow="autoplay"></iframe> | ||||
|             </div> | ||||
|             <script> | ||||
|                 if ('serviceWorker' in navigator) { | ||||
|                     window.addEventListener('load', function() { | ||||
|                         navigator.serviceWorker.register('/service-worker.js').then(function(registration) { | ||||
|                             console.log('ServiceWorker registration successful with scope: ', registration.scope); | ||||
|                         }, function(err) { | ||||
|                             console.log('ServiceWorker registration failed: ', err); | ||||
|                         }); | ||||
|                     }); | ||||
|                 } | ||||
|             </script> | ||||
|         </body> | ||||
|         </html> | ||||
|         ''') | ||||
| 
 | ||||
| # Generate manifest.json | ||||
| def generate_manifest_json(): | ||||
|     manifest = { | ||||
|         "name": "MidTownPlaydio", | ||||
|         "short_name": "MidTownPlaydio", | ||||
|         "start_url": "/", | ||||
|         "display": "standalone", | ||||
|         "background_color": "#ffffff", | ||||
|         "theme_color": "#ffffff", | ||||
|         "icons": [ | ||||
|             { | ||||
|                 "src": "/icon.png", | ||||
|                 "sizes": "192x192", | ||||
|                 "type": "image/png" | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     with open(os.path.join(STATIC_DIR, 'manifest.json'), 'w') as f: | ||||
|         json.dump(manifest, f, indent=4) | ||||
| 
 | ||||
| # Generate service-worker.js | ||||
| def generate_service_worker(): | ||||
|     with open(os.path.join(STATIC_DIR, 'service-worker.js'), 'w') as f: | ||||
|         f.write(''' | ||||
|         const CACHE_NAME = 'midtownplaydio-cache-v1'; | ||||
|         const urlsToCache = [ | ||||
|             '/', | ||||
|             '/index.html', | ||||
|             '/manifest.json', | ||||
|             '/icon.png' | ||||
|         ]; | ||||
| 
 | ||||
|         self.addEventListener('install', event => { | ||||
|             event.waitUntil( | ||||
|                 caches.open(CACHE_NAME).then(cache => { | ||||
|                     console.log('Opened cache'); | ||||
|                     return cache.addAll(urlsToCache); | ||||
|                 }) | ||||
|             ); | ||||
|         }); | ||||
| 
 | ||||
|         self.addEventListener('fetch', event => { | ||||
|             event.respondWith( | ||||
|                 caches.match(event.request).then(response => { | ||||
|                     // Cache hit - return response | ||||
|                     if (response) { | ||||
|                         return response; | ||||
|                     } | ||||
|                     return fetch(event.request); | ||||
|                 }) | ||||
|             ); | ||||
|         }); | ||||
| 
 | ||||
|         self.addEventListener('activate', event => { | ||||
|             const cacheWhitelist = [CACHE_NAME]; | ||||
|             event.waitUntil( | ||||
|                 caches.keys().then(cacheNames => { | ||||
|                     return Promise.all( | ||||
|                         cacheNames.map(cacheName => { | ||||
|                             if (cacheWhitelist.indexOf(cacheName) === -1) { | ||||
|                                 return caches.delete(cacheName); | ||||
|                             } | ||||
|                         }) | ||||
|                     ); | ||||
|                 }) | ||||
|             ); | ||||
|         }); | ||||
|         ''') | ||||
| 
 | ||||
| # Create a simple placeholder icon | ||||
| def create_placeholder_icon(): | ||||
|     # This is just a placeholder - in a real app, you would use a proper icon file | ||||
|     with open(os.path.join(STATIC_DIR, 'icon.png'), 'w') as f: | ||||
|         f.write("Placeholder for icon") | ||||
| 
 | ||||
| # Generate all static files on startup | ||||
| @app.on_event("startup") | ||||
| async def startup_event(): | ||||
|     generate_index_html() | ||||
|     generate_manifest_json() | ||||
|     generate_service_worker() | ||||
|     create_placeholder_icon() | ||||
| 
 | ||||
| # Mount static files | ||||
| app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") | ||||
| 
 | ||||
| # Health check endpoint | ||||
| @app.get("/health", status_code=200) | ||||
| async def health_check(): | ||||
|     return {"status": "ok"} | ||||
| 
 | ||||
| # If the file is executed directly, run a development server | ||||
| if __name__ == "__main__": | ||||
|     import uvicorn | ||||
|     uvicorn.run(app, host="0.0.0.0", port=3000)  | ||||
|  | @ -0,0 +1,5 @@ | |||
| fastapi==0.104.0 | ||||
| uvicorn==0.23.2 | ||||
| gunicorn==21.2.0 | ||||
| uvloop==0.18.0 | ||||
| httptools==0.6.0  | ||||
		Loading…
	
		Reference in New Issue
	
	 Your Name
						Your Name