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