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