Files
freno-dev/src/lib/deployment-detection.ts
2026-01-07 20:53:21 -05:00

197 lines
5.2 KiB
TypeScript

/**
* Deployment Detection System
* Detects when the app has been updated on the server
*/
const VERSION_CHECK_INTERVAL = 5 * 60 * 1000; // Check every 5 minutes
const VERSION_STORAGE_KEY = "app-version-hash";
/**
* Get a simple hash from the current page HTML
* This changes when deployment happens
*/
function getCurrentVersionHash(): string {
try {
// Use a combination of script tags to detect version
const scripts = Array.from(document.querySelectorAll("script[src]"))
.map((s) => (s as HTMLScriptElement).src)
.filter((src) => src.includes("/_build/"))
.sort()
.join(",");
// Simple hash function
let hash = 0;
for (let i = 0; i < scripts.length; i++) {
const char = scripts.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
} catch (e) {
console.warn("[Version Detection] Failed to get version hash:", e);
return "";
}
}
/**
* Check if a new version is available
* Returns true if new version detected
*/
async function checkForNewVersion(): Promise<boolean> {
try {
// Fetch current page HTML
const response = await fetch(window.location.pathname, {
method: "HEAD",
cache: "no-cache"
});
if (!response.ok) {
console.warn("[Version Detection] Health check failed:", response.status);
return false;
}
// Check if ETag changed (Vercel sets this)
const newEtag = response.headers.get("etag");
const storedEtag = sessionStorage.getItem("app-etag");
if (storedEtag && newEtag && storedEtag !== newEtag) {
console.log(
"[Version Detection] New version detected (ETag changed)",
storedEtag,
"→",
newEtag
);
return true;
}
// Store current ETag for future checks
if (newEtag) {
sessionStorage.setItem("app-etag", newEtag);
}
return false;
} catch (error) {
console.warn("[Version Detection] Version check failed:", error);
return false;
}
}
/**
* Show update notification to user
*/
function showUpdateNotification(): void {
// Only show once per session
if (sessionStorage.getItem("update-notification-shown")) {
return;
}
sessionStorage.setItem("update-notification-shown", "true");
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #3b82f6;
color: white;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
z-index: 9999;
max-width: 320px;
animation: slideIn 0.3s ease-out;
`;
notification.innerHTML = `
<style>
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div>
<strong>🎉 New Update Available</strong><br>
<span style="font-size: 13px; opacity: 0.9;">A new version of the app is ready.</span>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="location.reload()" style="
background: white;
color: #3b82f6;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
flex: 1;
">Update Now</button>
<button onclick="this.closest('div').parentElement.parentElement.remove()" style="
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">Later</button>
</div>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 30 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.style.animation = "slideIn 0.3s ease-out reverse";
setTimeout(() => notification.remove(), 300);
}
}, 30000);
}
/**
* Start monitoring for new deployments
*/
export function startDeploymentMonitoring(): void {
if (typeof window === "undefined") return;
// Store initial version
const initialVersion = getCurrentVersionHash();
sessionStorage.setItem(VERSION_STORAGE_KEY, initialVersion);
// Periodic version check
const intervalId = setInterval(async () => {
const hasNewVersion = await checkForNewVersion();
if (hasNewVersion) {
showUpdateNotification();
}
}, VERSION_CHECK_INTERVAL);
// Check on visibility change (user returns to tab)
const handleVisibilityChange = async () => {
if (document.visibilityState === "visible") {
const hasNewVersion = await checkForNewVersion();
if (hasNewVersion) {
showUpdateNotification();
}
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
// Cleanup function
if (typeof window !== "undefined") {
(window as any).__cleanupDeploymentMonitoring = () => {
clearInterval(intervalId);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}
}