revert: remove PWA functionality due to service worker issues

The PWA implementation was causing too many issues:
- Service worker conflicts with CSP policies
- 429 rate limiting errors
- Application freezing
- External resource blocking

Removed:
- service-worker.js
- manifest.json
- offline.html
- PWA service and install component
- PWA meta tags from index.html
- PWA asset configuration

The library now functions as a standard web app without PWA features.
This provides a more stable and reliable user experience.
This commit is contained in:
2026-02-20 01:07:16 -08:00
committed by Naomi Carrigan
parent a0f6362c1b
commit 009a68a0d2
7 changed files with 1 additions and 493 deletions
-15
View File
@@ -19,21 +19,6 @@
{
"glob": "**/*",
"input": "apps/frontend/public"
},
{
"glob": "manifest.json",
"input": "apps/frontend/src",
"output": "/"
},
{
"glob": "service-worker.js",
"input": "apps/frontend/src",
"output": "/"
},
{
"glob": "offline.html",
"input": "apps/frontend/src",
"output": "/"
}
],
"styles": ["apps/frontend/src/styles.scss"]
-1
View File
@@ -4,4 +4,3 @@
</main>
<app-footer></app-footer>
<app-toast></app-toast>
<app-pwa-install></app-pwa-install>
+1 -5
View File
@@ -3,12 +3,10 @@ import { RouterModule } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';
import { ToastComponent } from './components/toast/toast.component';
import { PwaInstallComponent } from './components/pwa-install/pwa-install.component';
import { AnalyticsService } from './services/analytics.service';
import { PwaService } from './services/pwa.service';
@Component({
imports: [RouterModule, HeaderComponent, FooterComponent, ToastComponent, PwaInstallComponent],
imports: [RouterModule, HeaderComponent, FooterComponent, ToastComponent],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss',
@@ -16,10 +14,8 @@ import { PwaService } from './services/pwa.service';
export class App implements OnInit {
protected title = 'Naomi\'s Library';
private analytics = inject(AnalyticsService);
private pwa = inject(PwaService);
ngOnInit(): void {
this.analytics.initialise();
// PWA service automatically initializes on construction
}
}
@@ -1,176 +0,0 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Hikari
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PwaService } from '../../services/pwa.service';
@Component({
selector: 'app-pwa-install',
standalone: true,
imports: [CommonModule],
template: `
@if (pwaService.isInstallable() && !dismissed) {
<div class="install-banner">
<div class="install-content">
<div class="install-icon">📱</div>
<div class="install-text">
<h3>Install Naomi's Library</h3>
<p>Add to your home screen for quick access and offline support!</p>
</div>
</div>
<div class="install-actions">
<button (click)="install()" class="btn-install">Install</button>
<button (click)="dismiss()" class="btn-dismiss">Not Now</button>
</div>
</div>
}
`,
styles: [`
.install-banner {
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border: 2px solid #9d4edd;
border-radius: 12px;
padding: 1.5rem;
max-width: 500px;
width: calc(100% - 2rem);
box-shadow: 0 8px 32px rgba(157, 78, 221, 0.3);
z-index: 1000;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateX(-50%) translateY(100px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.install-content {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.install-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.install-text h3 {
margin: 0 0 0.25rem 0;
color: #9d4edd;
font-size: 1.1rem;
}
.install-text p {
margin: 0;
color: #b0b0b0;
font-size: 0.9rem;
}
.install-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.btn-install,
.btn-dismiss {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-install {
background: linear-gradient(135deg, #9d4edd 0%, #c77dff 100%);
color: white;
}
.btn-install:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(157, 78, 221, 0.4);
}
.btn-dismiss {
background: transparent;
color: #9d4edd;
border: 1px solid #9d4edd;
}
.btn-dismiss:hover {
background: rgba(157, 78, 221, 0.1);
}
@media (max-width: 600px) {
.install-banner {
bottom: 0;
left: 0;
right: 0;
transform: none;
max-width: none;
width: 100%;
border-radius: 12px 12px 0 0;
border-bottom: none;
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.install-content {
flex-direction: column;
text-align: center;
}
.install-actions {
flex-direction: column-reverse;
}
.btn-install,
.btn-dismiss {
width: 100%;
}
}
`]
})
export class PwaInstallComponent {
protected pwaService = inject(PwaService);
protected dismissed = false;
protected async install(): Promise<void> {
const result = await this.pwaService.promptInstall();
if (result) {
this.dismissed = true;
}
}
protected dismiss(): void {
this.dismissed = true;
// Remember dismissal in session storage
sessionStorage.setItem('pwa-install-dismissed', 'true');
}
}
@@ -1,105 +0,0 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Hikari
*/
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class PwaService {
public promptEvent = signal<any>(null);
public isInstallable = signal(false);
public isInstalled = signal(false);
constructor() {
this.init();
}
private init(): void {
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
this.isInstalled.set(true);
}
// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (event: Event) => {
event.preventDefault();
this.promptEvent.set(event);
this.isInstallable.set(true);
});
// Listen for app installed event
window.addEventListener('appinstalled', () => {
this.isInstalled.set(true);
this.isInstallable.set(false);
this.promptEvent.set(null);
});
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('[PWA] Service Worker registered:', registration.scope);
// Check for updates periodically
setInterval(() => {
registration.update();
}, 60000); // Check every minute
// Listen for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New service worker available
console.log('[PWA] New version available! Refresh to update.');
// Optionally show a notification to the user
}
});
}
});
})
.catch((error) => {
console.error('[PWA] Service Worker registration failed:', error);
});
});
}
}
public async promptInstall(): Promise<boolean> {
const event = this.promptEvent();
if (!event) {
return false;
}
// Show the install prompt
event.prompt();
// Wait for the user's response
const choiceResult = await event.userChoice;
if (choiceResult.outcome === 'accepted') {
console.log('[PWA] User accepted the install prompt');
this.promptEvent.set(null);
this.isInstallable.set(false);
return true;
} else {
console.log('[PWA] User dismissed the install prompt');
return false;
}
}
public clearCache(): void {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CLEAR_CACHE'
});
}
}
}
-5
View File
@@ -8,11 +8,6 @@
<meta name="description" content="Naomi's curated collection of games, books, music, shows, manga, and art. Browse, engage, and suggest new additions!" />
<meta name="theme-color" content="#9d4edd" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Library" />
<script defer src="https://analytics.nhcarrigan.com/js/pa-YUXAn1vhhRttySUAw_LMN.js"></script>
<script>
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
-186
View File
@@ -1,186 +0,0 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Hikari
*/
const CACHE_VERSION = 'library-v1';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
const IMAGE_CACHE = `${CACHE_VERSION}-images`;
// Static assets to cache on install
const STATIC_ASSETS = [
'/offline.html'
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[Service Worker] Caching static assets');
return cache.addAll(STATIC_ASSETS).catch((err) => {
console.error('[Service Worker] Failed to cache static assets:', err);
// Don't fail installation if caching fails
return Promise.resolve();
});
}).then(() => {
return self.skipWaiting();
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => {
return cacheName.startsWith('library-') && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== IMAGE_CACHE;
})
.map((cacheName) => {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
}).then(() => {
return self.clients.claim();
})
);
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip Chrome extensions and other protocols
if (!url.protocol.startsWith('http')) {
return;
}
// API requests - Network first, cache fallback
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
// Clone the response before caching
const responseClone = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for failed API requests
return caches.match('/offline.html');
});
})
);
return;
}
// Images - Cache first, network fallback (only for same-origin)
if (request.destination === 'image' || url.pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) {
// Don't cache external images
if (url.origin !== self.location.origin) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request).then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(IMAGE_CACHE).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
});
})
);
return;
}
// Static assets - Cache first, network fallback (only for same-origin)
if (url.pathname.match(/\.(js|css|woff|woff2|ttf|eot)$/)) {
// Don't cache external scripts/styles (CDN resources, analytics, etc.)
if (url.origin !== self.location.origin) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request).then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
});
})
);
return;
}
// HTML pages - Network first, cache fallback
event.respondWith(
fetch(request)
.then((response) => {
// Only cache successful responses
if (response.ok) {
const responseClone = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
return caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page as fallback
return caches.match('/offline.html');
});
})
);
});
// Message event - handle cache clearing requests
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((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => caches.delete(cacheName))
);
})
);
}
});