feat: Add complete Docker deployment with web-based setup wizard

Major additions:
- Web-based setup wizard (setup.php, setup_wizard.php, setup-wizard.js)
- Production Docker configuration (docker-compose.prod.yml, .env.production)
- Database initialization SQL files (deploy/init_settings.sql)
- Template builder system with drag-and-drop UI
- Advanced features (OAuth, CDN, enhanced analytics, monetization)
- Comprehensive documentation (deployment guides, quick start, feature docs)
- Design system with accessibility and responsive layout
- Deployment automation scripts (deploy.ps1, generate-secrets.ps1)

Setup wizard allows customization of:
- Platform name and branding
- Domain configuration
- Membership tiers and pricing
- Admin credentials
- Feature toggles

Database includes 270+ tables for complete video streaming platform with
advanced features for analytics, moderation, template building, and monetization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
SamiAhmed7777
2025-10-26 01:42:31 -07:00
parent 0b7e2d0a5b
commit d22b3e1c0d
90 changed files with 22329 additions and 268 deletions

318
sw.js
View File

@@ -1,59 +1,307 @@
// EasyStream Service Worker (lightweight, safe defaults)
const CACHE_VERSION = 'es-v1';
// EasyStream Service Worker - Enhanced PWA v2.0
const CACHE_VERSION = 'es-v2';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const IMAGE_CACHE = `${CACHE_VERSION}-images`;
const FONT_CACHE = `${CACHE_VERSION}-fonts`;
const VIDEO_CACHE = `${CACHE_VERSION}-video`;
// Assets to precache
const PRECACHE = [
'/index.js',
'/manifest.json'
'/manifest.json',
'/f_scripts/shared/design-system.css',
'/f_scripts/shared/accessibility.css',
'/f_scripts/shared/responsive.css',
'/f_scripts/shared/themes.css'
];
// Cache size limits
const CACHE_LIMITS = {
images: 50,
fonts: 20,
video: 5
};
// Install event - precache critical assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker v2.0...');
event.waitUntil((async () => {
const cache = await caches.open(STATIC_CACHE);
await cache.addAll(PRECACHE);
self.skipWaiting();
try {
const cache = await caches.open(STATIC_CACHE);
await cache.addAll(PRECACHE);
console.log('[SW] Precached static assets');
self.skipWaiting();
} catch (error) {
console.error('[SW] Install failed:', error);
}
})());
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker v2.0...');
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter(k => !k.startsWith(CACHE_VERSION)).map(k => caches.delete(k)));
await Promise.all(
keys
.filter(k => !k.startsWith(CACHE_VERSION))
.map(k => {
console.log('[SW] Deleting old cache:', k);
return caches.delete(k);
})
);
self.clients.claim();
console.log('[SW] Activated and claimed clients');
})());
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const url = event.request.url;
// never cache uploads or HLS segments/manifests
if (url.includes('upload') || url.includes('uploader') || url.includes('index.m3u8') || url.includes('.ts')) {
return; // bypass SW
const { request } = event;
const url = new URL(request.url);
// Skip caching for:
// - Uploads
// - HLS video segments
// - API calls that should always be fresh
// - POST/PUT/DELETE requests
if (
url.pathname.includes('/upload') ||
url.pathname.includes('/uploader') ||
url.pathname.includes('.m3u8') ||
url.pathname.includes('.ts') ||
url.pathname.includes('/api/') ||
request.method !== 'GET'
) {
return; // Network only
}
// Navigation requests: network-first with cache fallback for basic offline shell
if (event.request.mode === 'navigate') {
event.respondWith((async () => {
try {
const fresh = await fetch(event.request);
return fresh;
} catch (e) {
const cache = await caches.open(STATIC_CACHE);
const shell = await cache.match('/index.js');
return shell || Response.error();
}
})());
// NAVIGATION REQUESTS: Network-first with offline fallback
if (request.mode === 'navigate') {
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
return;
}
// Others: stale-while-revalidate
event.respondWith((async () => {
const cached = await caches.match(event.request);
const fetchPromise = fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.ok && event.request.method === 'GET') {
const copy = networkResponse.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, copy)).catch(() => {});
}
return networkResponse;
}).catch(() => cached);
return cached || fetchPromise;
})());
// IMAGES: Cache-first with network fallback
if (request.destination === 'image') {
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images));
return;
}
// FONTS: Cache-first (fonts rarely change)
if (request.destination === 'font' || url.pathname.match(/\.(woff2?|ttf|eot)$/)) {
event.respondWith(cacheFirstStrategy(request, FONT_CACHE, CACHE_LIMITS.fonts));
return;
}
// CSS/JS: Stale-while-revalidate
if (request.destination === 'style' || request.destination === 'script') {
event.respondWith(staleWhileRevalidate(request, STATIC_CACHE));
return;
}
// VIDEO THUMBNAILS: Cache-first
if (url.pathname.match(/thumb|thumbnail|preview/i) && url.pathname.match(/\.(jpg|jpeg|png|webp)$/)) {
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images));
return;
}
// DEFAULT: Network-first
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
});
// STRATEGY: Network-first with cache fallback
async function networkFirstStrategy(request, cacheName) {
try {
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.ok) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return new Response(getOfflineHTML(), {
headers: { 'Content-Type': 'text/html' }
});
}
return Response.error();
}
}
// STRATEGY: Cache-first with network fallback
async function cacheFirstStrategy(request, cacheName, limit) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.ok) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
// Limit cache size
if (limit) {
limitCacheSize(cacheName, limit);
}
}
return networkResponse;
} catch (error) {
return Response.error();
}
}
// STRATEGY: Stale-while-revalidate
async function staleWhileRevalidate(request, cacheName) {
const cachedResponse = await caches.match(request);
const fetchPromise = fetch(request).then((networkResponse) => {
if (networkResponse && networkResponse.ok) {
const cache = caches.open(cacheName);
cache.then(c => c.put(request, networkResponse.clone()));
}
return networkResponse;
}).catch(() => cachedResponse);
return cachedResponse || fetchPromise;
}
// Limit cache size to prevent unlimited growth
async function limitCacheSize(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
// Delete oldest entries (FIFO)
const deleteCount = keys.length - maxItems;
for (let i = 0; i < deleteCount; i++) {
await cache.delete(keys[i]);
}
}
}
// Offline page HTML
function getOfflineHTML() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - EasyStream</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #121212;
color: #f0f0f0;
text-align: center;
padding: 20px;
}
.offline-container {
max-width: 500px;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
color: #06a2cb;
}
p {
font-size: 1.125rem;
line-height: 1.6;
margin-bottom: 2rem;
color: #d0d0d0;
}
.retry-btn {
display: inline-block;
padding: 12px 24px;
background: #06a2cb;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background 200ms;
}
.retry-btn:hover {
background: #92cefb;
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="icon">📡</div>
<h1>You're Offline</h1>
<p>It looks like you've lost your internet connection. Please check your network and try again.</p>
<a href="javascript:location.reload()" class="retry-btn">Retry</a>
</div>
</body>
</html>
`;
}
// Handle messages from the main thread
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))))
);
}
});
// Background sync for offline actions (if supported)
if ('sync' in self.registration) {
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-watch-history') {
event.waitUntil(syncWatchHistory());
}
});
}
async function syncWatchHistory() {
// Placeholder for syncing watch history when back online
console.log('[SW] Syncing watch history...');
}
// Push notifications support
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'New notification from EasyStream',
icon: '/android-icon-192x192.svg',
badge: '/android-icon-96x96.svg',
vibrate: [200, 100, 200],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
};
event.waitUntil(
self.registration.showNotification('EasyStream', options)
);
});
// Notification click handler
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});
console.log('[SW] Service Worker v2.0 loaded');