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