Fix PWA-Reload: __BUILD_TIME__ und version.txt aus selber Quelle; Fix QuickLinkIcon custom icons
This commit is contained in:
@@ -455,17 +455,22 @@ function QuickLinksCarousel({ links }) {
|
||||
function QuickLinkIcon({ l }) {
|
||||
const [imgOk, setImgOk] = useState(true);
|
||||
|
||||
// Favicon-URL bauen: gespeicherten Wert nehmen oder aus URL ableiten
|
||||
// Favicon nur wenn kein eigenes Icon gesetzt (Standard-🔗) oder bereits eine URL gespeichert ist
|
||||
const useDefaultFavicon = !l.icon || l.icon === '🔗';
|
||||
const faviconUrl = (() => {
|
||||
if (l.icon && l.icon.startsWith('http')) return l.icon;
|
||||
try {
|
||||
const host = new URL(l.url).hostname;
|
||||
return `https://www.google.com/s2/favicons?sz=64&domain=${host}`;
|
||||
} catch { return null; }
|
||||
if (useDefaultFavicon) {
|
||||
try {
|
||||
const host = new URL(l.url).hostname;
|
||||
return `https://www.google.com/s2/favicons?sz=64&domain=${host}`;
|
||||
} catch { return null; }
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
const showEmoji = !faviconUrl || !imgOk;
|
||||
const fallbackEmoji = l.icon && !l.icon.startsWith('http') ? l.icon : '🔗';
|
||||
const showImg = !!faviconUrl && imgOk;
|
||||
const showEmoji = !showImg;
|
||||
const emoji = (l.icon && !l.icon.startsWith('http')) ? l.icon : '🔗';
|
||||
|
||||
return (
|
||||
<a href={l.url} target="_blank" rel="noopener noreferrer"
|
||||
@@ -473,10 +478,10 @@ function QuickLinkIcon({ l }) {
|
||||
padding:'4px 2px', background:'rgba(255,255,255,0.04)',
|
||||
border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
|
||||
textDecoration:'none', height:48, width:'100%', boxSizing:'border-box', cursor:'pointer' }}>
|
||||
{!showEmoji
|
||||
{showImg
|
||||
? <img src={faviconUrl} alt="" style={{width:18,height:18,objectFit:'contain'}}
|
||||
onError={() => setImgOk(false)}/>
|
||||
: <span style={{ fontSize:16 }}>{fallbackEmoji}</span>
|
||||
: <span style={{ fontSize:16 }}>{emoji}</span>
|
||||
}
|
||||
<span style={{ color:'rgba(255,255,255,0.65)', fontSize:8, fontFamily:'monospace',
|
||||
maxWidth:52, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span>
|
||||
|
||||
@@ -3,17 +3,13 @@ import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
// ── Auto-Reload bei neuem Build ───────────────────────────────────────────────
|
||||
// Regeln:
|
||||
// - Neuer Build wird erkannt → nur Flag setzen (window.__newBuildAvailable)
|
||||
// - Reload passiert NUR:
|
||||
// a) beim Öffnen der App (App kommt aus Hintergrund in den Vordergrund)
|
||||
// b) beim Klick auf "Dashboard" (in App.jsx geprüft)
|
||||
// - NIEMALS während einer Eingabe oder bei ungespeicherten Feldern
|
||||
// __BUILD_TIME__ wird von Vite zur Build-Zeit aus version.txt gebacken.
|
||||
// /api/build-time liefert denselben Wert vom Server.
|
||||
// Wenn sie abweichen → neues Build wurde deployed → Reload.
|
||||
// Kein localStorage, keine Race-Condition.
|
||||
const MY_BUILD_TIME = __BUILD_TIME__;
|
||||
window.__newBuildAvailable = false;
|
||||
|
||||
const STORED_KEY = 'dd_build_time';
|
||||
|
||||
// Läuft gerade eine Eingabe? (Fokus auf Eingabefeld)
|
||||
function isUserTyping() {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
@@ -21,7 +17,6 @@ function isUserTyping() {
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||
}
|
||||
|
||||
// Stehen irgendwo ungespeicherte Eingaben in Textfeldern?
|
||||
function hasUnsavedInput() {
|
||||
const fields = document.querySelectorAll('input, textarea');
|
||||
for (const f of fields) {
|
||||
@@ -40,21 +35,20 @@ async function getBuildTime() {
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// Prüft die Build-Zeit und setzt nur das Flag (lädt nie von selbst)
|
||||
async function checkBuild() {
|
||||
const bt = await getBuildTime();
|
||||
if (!bt) return;
|
||||
const stored = localStorage.getItem(STORED_KEY);
|
||||
if (!stored) {
|
||||
localStorage.setItem(STORED_KEY, bt);
|
||||
} else if (stored !== bt) {
|
||||
localStorage.setItem(STORED_KEY, bt);
|
||||
// Prüft ob Server-Build != Bundle-Build → Flag setzen
|
||||
// reloadNow: sofort laden wenn kein Input aktiv (für App-Fokus)
|
||||
async function checkBuild({ reloadNow = false } = {}) {
|
||||
const serverBt = await getBuildTime();
|
||||
if (!serverBt) return;
|
||||
if (serverBt !== MY_BUILD_TIME) {
|
||||
window.__newBuildAvailable = true;
|
||||
if (reloadNow && !isUserTyping() && !hasUnsavedInput()) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sicherer Reload: nur wenn neuer Build da UND keine Eingabe aktiv/ungespeichert
|
||||
// Wird von App.jsx beim Dashboard-Klick aufgerufen. Gibt true zurück wenn neu geladen wird.
|
||||
// Sicherer Reload bei Seitenwechsel (aufgerufen aus App.jsx setActive)
|
||||
window.__safeReloadIfNewBuild = function () {
|
||||
if (!window.__newBuildAvailable) return false;
|
||||
if (isUserTyping() || hasUnsavedInput()) return false;
|
||||
@@ -62,32 +56,26 @@ window.__safeReloadIfNewBuild = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Beim Start einmal prüfen (App-Öffnen Fall 1: Kaltstart)
|
||||
// Beim Start prüfen (kein sofortiger Reload – Seite lädt gerade erst)
|
||||
checkBuild();
|
||||
// Im Hintergrund alle 60s nur das Flag aktualisieren
|
||||
// Im Hintergrund alle 60s
|
||||
setInterval(() => checkBuild(), 60_000);
|
||||
|
||||
// App-Öffnen Fall 2: App kommt aus dem Hintergrund zurück in den Vordergrund
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
// App kommt in den Vordergrund → prüfen und bei Bedarf sofort laden
|
||||
// (PWA vom Homescreen öffnen / aus dem Hintergrund holen)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
await checkBuild();
|
||||
// Nur laden wenn der User nicht gerade tippt und nichts ungespeichert ist
|
||||
if (!isUserTyping() && !hasUnsavedInput()) {
|
||||
window.__safeReloadIfNewBuild();
|
||||
}
|
||||
checkBuild({ reloadNow: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Service Worker ────────────────────────────────────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (e) => {
|
||||
if (e.data?.type === 'SW_ACTIVATED') {
|
||||
window.__newBuildAvailable = true; // nur merken
|
||||
}
|
||||
if (e.data?.type === 'SW_ACTIVATED') window.__newBuildAvailable = true;
|
||||
});
|
||||
|
||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||
// Browser regelmäßig nach neuem SW suchen lassen
|
||||
setInterval(() => reg.update().catch(()=>{}), 60_000);
|
||||
if (reg.waiting) reg.waiting.postMessage('SKIP_WAITING');
|
||||
reg.addEventListener('updatefound', () => {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const now = new Date();
|
||||
const buildTime = now.toLocaleString('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
// version.txt wird im Dockerfile VOR dem Build generiert (date +%s)
|
||||
// Damit haben __BUILD_TIME__ im Bundle und /api/build-time exakt denselben Wert
|
||||
let buildTime;
|
||||
try {
|
||||
buildTime = readFileSync('./version.txt', 'utf8').trim();
|
||||
} catch {
|
||||
// Fallback für lokale Entwicklung ohne Dockerfile
|
||||
buildTime = String(Date.now());
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
||||
Reference in New Issue
Block a user