Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/ratewise-precache-cold-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@app/ratewise': patch
---

重整 PWA 快取為優先級分層,確保主功能(幣別換算)離線必可載入,並讓離線錯誤頁不再無謂出現:

- Tier 1 預快取只保留 app shell(index.html)、JS/CSS、路由 loader 清單與少量 shell 圖示與離線頁,precache 由 428 筆(約 34.5MB)降至 92 筆(約 2.2MB),弱網下 Service Worker 能可靠安裝完成。
- Tier 2 改為 runtime 快取:大型圖片(CacheFirst)、SEO 頁(導覽回退至 app shell)、匯率資料(StaleWhileRevalidate 7 天離線備援)。
- 離線時一律以 app shell 還原主功能,offline.html 僅作最後手段,不再因預快取過大而誤觸。
5 changes: 5 additions & 0 deletions .changeset/ratewise-precache-guardrails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

強化 PWA 快取分層的離線韌性與防回歸護欄:iOS 快取被驅逐後一併修復離線導覽所需的路由 loader 清單、擴充匯率離線備援的快取容量,並收緊預快取守門避免非必要資源回流,確保主功能離線載入更穩定。
232 changes: 25 additions & 207 deletions apps/ratewise/public/offline.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,184 +2,13 @@
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#7C3AED" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#8B5CF6" />
<title>離線模式 - HaoRate</title>
<!-- [fix:2026-01-08] 使用相對路徑,確保在任何 base path 下都能正確載入 -->
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<script>
(function () {
var themeColors = {
zen: '#7C3AED',
nitro: '#0096E6',
kawaii: '#BE185D',
classic: '#8B4513',
ocean: '#06B6D4',
forest: '#059669',
};
var supportedStyles = Object.keys(themeColors);
var selectedStyle = 'zen';

try {
var storedTheme = localStorage.getItem('ratewise-theme');
if (storedTheme) {
var parsedTheme = JSON.parse(storedTheme);
if (supportedStyles.indexOf(parsedTheme.style) >= 0) {
selectedStyle = parsedTheme.style;
}
}
} catch (e) {
selectedStyle = 'zen';
}

document.documentElement.dataset.style = selectedStyle;

var themeColor = document.querySelector('meta[name="theme-color"]');
if (themeColor) {
themeColor.setAttribute('content', themeColors[selectedStyle] || themeColors.zen);
}
})();
</script>
<style>
html[data-style='zen'] {
--offline-theme-color: #7c3aed;
--offline-background: #f8fafc;
--offline-surface: #ffffff;
--offline-border: #e2e8f0;
--offline-text: #0f172a;
--offline-text-muted: #64748b;
--offline-primary: #7c3aed;
--offline-primary-foreground: #ffffff;
--offline-secondary: #6366f1;
--offline-accent: #8b5cf6;
--offline-primary-tint: rgba(124, 58, 237, 0.08);
--offline-primary-border-tint: rgba(124, 58, 237, 0.18);
--offline-primary-shadow-soft: rgba(124, 58, 237, 0.18);
--offline-primary-shadow-strong: rgba(124, 58, 237, 0.26);
--offline-primary-shadow-hover: rgba(124, 58, 237, 0.34);
--offline-warning-tint: rgba(245, 158, 11, 0.16);
--offline-warning: #f59e0b;
--offline-success-tint: rgba(34, 197, 94, 0.12);
--offline-success: #22c55e;
}

html[data-style='nitro'] {
--offline-theme-color: #0096e6;
--offline-background: #020617;
--offline-surface: #0f172a;
--offline-border: #1e293b;
--offline-text: #ffffff;
--offline-text-muted: #cbd5e1;
--offline-primary: #0096e6;
--offline-primary-foreground: #020617;
--offline-secondary: #818cf8;
--offline-accent: #00ff88;
--offline-primary-tint: rgba(0, 150, 230, 0.08);
--offline-primary-border-tint: rgba(0, 150, 230, 0.18);
--offline-primary-shadow-soft: rgba(0, 150, 230, 0.18);
--offline-primary-shadow-strong: rgba(0, 150, 230, 0.26);
--offline-primary-shadow-hover: rgba(0, 150, 230, 0.34);
--offline-warning-tint: rgba(251, 191, 36, 0.16);
--offline-warning: #fbbf24;
--offline-success-tint: rgba(52, 211, 153, 0.12);
--offline-success: #34d399;
}

html[data-style='kawaii'] {
--offline-theme-color: #be185d;
--offline-background: #fffaf4;
--offline-surface: #ffffff;
--offline-border: #ffe4e1;
--offline-text: #5c2d3c;
--offline-text-muted: #66414c;
--offline-primary: #be185d;
--offline-primary-foreground: #ffffff;
--offline-secondary: #be185d;
--offline-accent: #ffb6c1;
--offline-primary-tint: rgba(190, 24, 93, 0.08);
--offline-primary-border-tint: rgba(190, 24, 93, 0.18);
--offline-primary-shadow-soft: rgba(190, 24, 93, 0.18);
--offline-primary-shadow-strong: rgba(190, 24, 93, 0.26);
--offline-primary-shadow-hover: rgba(190, 24, 93, 0.34);
--offline-warning-tint: rgba(253, 224, 71, 0.16);
--offline-warning: #fde047;
--offline-success-tint: rgba(134, 239, 172, 0.12);
--offline-success: #86efac;
}

html[data-style='classic'] {
--offline-theme-color: #8b4513;
--offline-background: #fffafb;
--offline-surface: #ffffff;
--offline-border: #f5e6dc;
--offline-text: #431407;
--offline-text-muted: #78503c;
--offline-primary: #8b4513;
--offline-primary-foreground: #ffffff;
--offline-secondary: #a16207;
--offline-accent: #b47850;
--offline-primary-tint: rgba(139, 69, 19, 0.08);
--offline-primary-border-tint: rgba(139, 69, 19, 0.18);
--offline-primary-shadow-soft: rgba(139, 69, 19, 0.18);
--offline-primary-shadow-strong: rgba(139, 69, 19, 0.26);
--offline-primary-shadow-hover: rgba(139, 69, 19, 0.34);
--offline-warning-tint: rgba(180, 83, 9, 0.16);
--offline-warning: #b45309;
--offline-success-tint: rgba(22, 163, 74, 0.12);
--offline-success: #16a34a;
}

html[data-style='ocean'] {
--offline-theme-color: #06b6d4;
--offline-background: #f0f9ff;
--offline-surface: #ffffff;
--offline-border: #bae6fd;
--offline-text: #075985;
--offline-text-muted: #164e63;
--offline-primary: #06b6d4;
--offline-primary-foreground: #020617;
--offline-secondary: #0ea5e9;
--offline-accent: #14b8a6;
--offline-primary-tint: rgba(6, 182, 212, 0.08);
--offline-primary-border-tint: rgba(6, 182, 212, 0.18);
--offline-primary-shadow-soft: rgba(6, 182, 212, 0.18);
--offline-primary-shadow-strong: rgba(6, 182, 212, 0.26);
--offline-primary-shadow-hover: rgba(6, 182, 212, 0.34);
--offline-warning-tint: rgba(245, 158, 11, 0.16);
--offline-warning: #f59e0b;
--offline-success-tint: rgba(20, 184, 166, 0.12);
--offline-success: #14b8a6;
}

html[data-style='forest'] {
--offline-theme-color: #059669;
--offline-background: #f0fdf4;
--offline-surface: #ffffff;
--offline-border: #bbf7d0;
--offline-text: #14532d;
--offline-text-muted: #166534;
--offline-primary: #059669;
--offline-primary-foreground: #020617;
--offline-secondary: #16a34a;
--offline-accent: #65a30d;
--offline-primary-tint: rgba(5, 150, 105, 0.08);
--offline-primary-border-tint: rgba(5, 150, 105, 0.18);
--offline-primary-shadow-soft: rgba(5, 150, 105, 0.18);
--offline-primary-shadow-strong: rgba(5, 150, 105, 0.26);
--offline-primary-shadow-hover: rgba(5, 150, 105, 0.34);
--offline-warning-tint: rgba(234, 179, 8, 0.16);
--offline-warning: #eab308;
--offline-success-tint: rgba(34, 197, 94, 0.12);
--offline-success: #22c55e;
}
html[data-style='nitro'] {
color-scheme: dark;
}

* {
margin: 0;
padding: 0;
Expand All @@ -190,58 +19,51 @@
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
var(--offline-background) 0%,
var(--offline-primary-tint) 50%,
var(--offline-surface) 100%
);
padding: calc(1rem + env(safe-area-inset-top, 0px)) 1rem
calc(1rem + env(safe-area-inset-bottom, 0px));
background: linear-gradient(135deg, #e8ecf4 0%, #f3e8ff 50%, #ede9fe 100%);
padding: 1rem;
text-align: center;
}

.container {
max-width: 400px;
padding: 2rem;
background: var(--offline-surface);
background: rgba(255, 255, 255, 0.9);
border-radius: 24px;
box-shadow: 0 8px 32px var(--offline-primary-shadow-soft);
border: 1px solid var(--offline-primary-border-tint);
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.1);
}

.icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
background: linear-gradient(135deg, var(--offline-primary) 0%, var(--offline-accent) 100%);
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px var(--offline-primary-shadow-strong);
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
}

.icon svg {
width: 48px;
height: 48px;
color: var(--offline-primary-foreground);
color: white;
}

h1 {
font-size: 1.5rem;
color: var(--offline-text);
color: #4c1d95;
margin-bottom: 0.75rem;
font-weight: 600;
}

p {
color: var(--offline-text-muted);
color: #6b7280;
line-height: 1.6;
margin-bottom: 1.5rem;
}
Expand All @@ -251,8 +73,8 @@
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--offline-warning-tint);
color: var(--offline-warning);
background: #fef3c7;
color: #92400e;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
Expand All @@ -263,7 +85,7 @@
content: '';
width: 8px;
height: 8px;
background: var(--offline-warning);
background: #f59e0b;
border-radius: 50%;
animation: pulse 2s infinite;
}
Expand All @@ -283,12 +105,8 @@
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(
135deg,
var(--offline-primary) 0%,
var(--offline-secondary) 100%
);
color: var(--offline-primary-foreground);
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
Expand All @@ -297,12 +115,12 @@
transition:
transform 0.2s,
box-shadow 0.2s;
box-shadow: 0 4px 12px var(--offline-primary-shadow-strong);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.retry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--offline-primary-shadow-hover);
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

.retry-btn:active {
Expand All @@ -312,12 +130,12 @@
.tips {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--offline-border);
border-top: 1px solid #e5e7eb;
}

.tips h2 {
font-size: 0.875rem;
color: var(--offline-text-muted);
color: #6b7280;
font-weight: 500;
margin-bottom: 0.75rem;
}
Expand All @@ -332,23 +150,23 @@
align-items: flex-start;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--offline-text);
color: #4b5563;
margin-bottom: 0.5rem;
}

.tips li::before {
content: '•';
color: var(--offline-primary);
color: #8b5cf6;
font-weight: bold;
}

.cached-data {
margin-top: 1rem;
padding: 0.75rem;
background: var(--offline-success-tint);
background: #f0fdf4;
border-radius: 8px;
font-size: 0.875rem;
color: var(--offline-success);
color: #166534;
}
</style>
</head>
Expand Down
Loading
Loading