commit
0f674eb3c7
|
|
@ -3,7 +3,7 @@ NoteDiscovery - Self-Hosted Markdown Knowledge Base
|
||||||
Main FastAPI application
|
Main FastAPI application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends, APIRouter
|
from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends, APIRouter, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
@ -171,6 +171,21 @@ plugin_manager.run_hook('on_app_startup')
|
||||||
static_path = Path(__file__).parent.parent / "frontend"
|
static_path = Path(__file__).parent.parent / "frontend"
|
||||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||||
|
|
||||||
|
# PWA Service Worker - must be served from root for proper scope
|
||||||
|
@app.get("/sw.js")
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def service_worker(request: Request):
|
||||||
|
"""Serve the PWA service worker from root path for proper scope.
|
||||||
|
Injects the app version from VERSION file for cache invalidation."""
|
||||||
|
sw_path = static_path / "sw.js"
|
||||||
|
if sw_path.exists():
|
||||||
|
async with aiofiles.open(sw_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = await f.read()
|
||||||
|
# Inject app version into cache name
|
||||||
|
content = content.replace('__APP_VERSION__', version)
|
||||||
|
return Response(content=content, media_type="application/javascript")
|
||||||
|
raise HTTPException(status_code=404, detail="Service worker not found")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Custom Exception Handlers
|
# Custom Exception Handlers
|
||||||
|
|
|
||||||
|
|
@ -875,6 +875,15 @@ function noteApp() {
|
||||||
if (this.showGraph) {
|
if (this.showGraph) {
|
||||||
setTimeout(() => this.initGraph(), 300);
|
setTimeout(() => this.initGraph(), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update PWA theme-color meta tag to match current theme
|
||||||
|
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (themeColorMeta) {
|
||||||
|
// Get the accent color from CSS variables
|
||||||
|
const accentColor = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--accent-primary').trim() || '#667eea';
|
||||||
|
themeColorMeta.setAttribute('content', accentColor);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load theme:', error);
|
console.error('Failed to load theme:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,20 @@
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon.svg">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon.svg">
|
||||||
<link rel="apple-touch-icon" href="/static/logo.svg">
|
<link rel="apple-touch-icon" href="/static/logo.svg">
|
||||||
|
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
|
||||||
|
<!-- PWA Meta Tags -->
|
||||||
|
<meta name="application-name" content="NoteDiscovery">
|
||||||
|
<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="NoteDiscovery">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<!-- Theme color: initial value, updated dynamically by app.js when theme changes -->
|
||||||
|
<meta name="theme-color" content="#667eea">
|
||||||
|
<meta name="msapplication-TileColor" content="#667eea">
|
||||||
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
|
|
||||||
<!-- Tailwind CSS from CDN (v3.4.17) -->
|
<!-- Tailwind CSS from CDN (v3.4.17) -->
|
||||||
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
||||||
|
|
||||||
|
|
@ -2816,6 +2830,21 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
|
|
||||||
|
<!-- PWA Service Worker Registration -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('SW registered:', registration.scope);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('SW registration failed:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@
|
||||||
<title>Login - NoteDiscovery</title>
|
<title>Login - NoteDiscovery</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
|
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
<meta name="theme-color" content="#667eea">
|
||||||
|
<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="NoteDiscovery">
|
||||||
|
|
||||||
<!-- Preload translations (same approach as index.html) -->
|
<!-- Preload translations (same approach as index.html) -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "NoteDiscovery",
|
||||||
|
"short_name": "Notes",
|
||||||
|
"description": "A beautiful, self-hosted markdown notes application",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0f172a",
|
||||||
|
"theme_color": "#667eea",
|
||||||
|
"orientation": "any",
|
||||||
|
"scope": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/logo.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/logo.svg",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/logo.svg",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["productivity", "utilities"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// NoteDiscovery Service Worker
|
||||||
|
// Minimal service worker for PWA install support
|
||||||
|
|
||||||
|
// Cache version - automatically uses app version from VERSION file
|
||||||
|
// Cache is invalidated when app version changes (e.g., 0.10.4 -> 0.10.5)
|
||||||
|
// This forces users to download fresh files when you release a new version.
|
||||||
|
const CACHE_NAME = 'notediscovery-__APP_VERSION__';
|
||||||
|
|
||||||
|
// Assets to cache for faster repeat visits
|
||||||
|
const PRECACHE_ASSETS = [
|
||||||
|
'/static/logo.svg',
|
||||||
|
'/static/favicon.svg',
|
||||||
|
'/static/app.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install event - cache essential assets
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => cache.addAll(PRECACHE_ASSETS))
|
||||||
|
.then(() => self.skipWaiting())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate event - clean up old caches
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames
|
||||||
|
.filter((name) => name !== CACHE_NAME)
|
||||||
|
.map((name) => caches.delete(name))
|
||||||
|
);
|
||||||
|
}).then(() => self.clients.claim())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch event - network first, fallback to cache for assets
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Only handle same-origin requests
|
||||||
|
if (url.origin !== location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For API calls, always go to network
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For static assets, try cache first then network
|
||||||
|
if (url.pathname.startsWith('/static/')) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then((cached) => cached || fetch(event.request))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For everything else, network first
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.catch(() => caches.match(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Reference in New Issue