diff --git a/js/components/error-boundary.js b/js/components/error-boundary.js
index 0efeac9..862754b 100644
--- a/js/components/error-boundary.js
+++ b/js/components/error-boundary.js
@@ -88,6 +88,42 @@ export function setupGlobalErrorHandler() {
console.error('Unhandled promise rejection:', event.reason)
showErrorToast(event.reason?.message || 'Ein Fehler ist aufgetreten')
})
+
+ // Offline/Online detection
+ setupOfflineIndicator()
+}
+
+/**
+ * Shows/hides offline indicator based on network status
+ */
+function setupOfflineIndicator() {
+ const updateStatus = () => {
+ let indicator = document.querySelector('.offline-indicator')
+
+ if (!navigator.onLine) {
+ if (!indicator) {
+ indicator = document.createElement('div')
+ indicator.className = 'offline-indicator'
+ indicator.innerHTML = `
+ 📡
+ Offline
+ `
+ document.body.appendChild(indicator)
+ requestAnimationFrame(() => indicator.classList.add('visible'))
+ }
+ } else {
+ if (indicator) {
+ indicator.classList.remove('visible')
+ setTimeout(() => indicator.remove(), 300)
+ }
+ }
+ }
+
+ window.addEventListener('online', updateStatus)
+ window.addEventListener('offline', updateStatus)
+
+ // Check on init
+ updateStatus()
}
/**
@@ -214,6 +250,40 @@ style.textContent = /* css */`
.error-toast-close:hover {
color: var(--color-text);
}
+
+ /* Offline Indicator */
+ .offline-indicator {
+ position: fixed;
+ top: var(--space-md);
+ left: 50%;
+ transform: translateX(-50%) translateY(-100px);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-md);
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-full);
+ box-shadow: var(--shadow-md);
+ z-index: 10001;
+ opacity: 0;
+ transition: transform 0.3s ease, opacity 0.3s ease;
+ }
+
+ .offline-indicator.visible {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+ }
+
+ .offline-indicator .offline-icon {
+ filter: grayscale(1);
+ }
+
+ .offline-indicator .offline-text {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text);
+ }
`
document.head.appendChild(style)
diff --git a/js/services/currency.js b/js/services/currency.js
index f8c3e91..2c0ba66 100644
--- a/js/services/currency.js
+++ b/js/services/currency.js
@@ -16,8 +16,8 @@ const CURRENCY_SYMBOLS = {
JPY: 'Â¥'
}
-const CACHE_DURATION = 10 * 60 * 1000 // 10 minutes (CoinGecko free tier: 10-30 req/min)
-const MIN_REQUEST_INTERVAL = 6 * 1000 // 6 seconds between requests (max ~10/min)
+const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes (CoinGecko free tier is strict)
+const MIN_REQUEST_INTERVAL = 60 * 1000 // 60 seconds between requests
let cachedRates = null
let cacheTimestamp = 0
diff --git a/service-worker.js b/service-worker.js
index 9dedd69..ef96578 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'dgray-v27';
+const CACHE_NAME = 'dgray-v28';
const STATIC_ASSETS = [
'/',
'/index.html',
@@ -16,6 +16,8 @@ const STATIC_ASSETS = [
'/js/components/chat-widget.js',
'/js/components/listing-card.js',
'/js/components/search-box.js',
+ '/js/components/error-boundary.js',
+ '/js/services/currency.js',
'/locales/de.json',
'/locales/en.json',
'/locales/fr.json',