From 4ac8d82c8147ed78514190d8ef43fcb66069da7c Mon Sep 17 00:00:00 2001 From: Yash Ghule Date: Tue, 28 Apr 2026 12:59:18 -0400 Subject: [PATCH] feat(pwa): add fullscreen installable PWA with manifest, service worker, and cross-platform app icons. (#1728) Integrated into release/v3.7.4 --- public/apple-touch-icon.png | Bin 0 -> 7230 bytes public/favicon.ico | Bin 0 -> 141845 bytes public/icon-512.png | Bin 0 -> 7230 bytes public/sw.js | 75 ++++++++++++++++++++++++++ src/app/layout.tsx | 28 +++++++++- src/app/manifest.ts | 29 ++++++++++ src/shared/components/PwaRegister.tsx | 17 ++++++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/icon-512.png create mode 100644 public/sw.js create mode 100644 src/app/manifest.ts create mode 100644 src/shared/components/PwaRegister.tsx diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4a0d1407f0f0db9dc10e3310d0c8893681cc5a GIT binary patch literal 7230 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 E0>SdMVE_OC literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b6fbae4f347500cfaa64a8a81ca14027b1995d6 GIT binary patch literal 141845 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 E0>SdMVE_OC literal 0 HcmV?d00001 diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000000..d667507629 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,75 @@ +const CACHE_NAME = "omniroute-pwa-v1"; +const APP_SHELL = [ + "/", + "/offline", + "/manifest.webmanifest", + "/icon-512.png", + "/apple-touch-icon.png", +]; +const EXCLUDED_PATH_PREFIXES = ["/api/", "/a2a", "/dashboard/endpoint"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(APP_SHELL)) + .then(() => self.skipWaiting()), + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") { + return; + } + + const requestUrl = new URL(event.request.url); + const isSameOrigin = requestUrl.origin === self.location.origin; + const isExcludedPath = EXCLUDED_PATH_PREFIXES.some((prefix) => + requestUrl.pathname.startsWith(prefix), + ); + const destination = event.request.destination; + const isStaticAsset = ["style", "script", "image", "font"].includes(destination); + const isNavigateRequest = event.request.mode === "navigate"; + + // Never cache API/dashboard traffic with potentially auth-sensitive content. + if (!isSameOrigin || isExcludedPath) { + return; + } + + event.respondWith( + (async () => { + if (isNavigateRequest) { + try { + return await fetch(event.request); + } catch { + return (await caches.match("/offline")) || Response.error(); + } + } + + if (!isStaticAsset) { + return fetch(event.request); + } + + const cachedResponse = await caches.match(event.request); + if (cachedResponse) { + return cachedResponse; + } + + const networkResponse = await fetch(event.request); + if (networkResponse && networkResponse.status === 200) { + const responseClone = networkResponse.clone(); + void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseClone)); + } + return networkResponse; + })(), + ); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e259b92c67..6168ea1a2f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,12 +5,19 @@ import { NextIntlClientProvider } from "next-intl"; import { getMessages, getLocale } from "next-intl/server"; import { RTL_LOCALES } from "@/i18n/config"; import { getSettings } from "@/lib/db/settings"; +import type { Viewport } from "next"; +import { PwaRegister } from "@/shared/components/PwaRegister"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", }); +export const viewport: Viewport = { + themeColor: "#0b0f1a", + viewportFit: "cover", +}; + export async function generateMetadata() { const settings = await getSettings(); const instanceName = settings?.instanceName || "OmniRoute"; @@ -20,9 +27,25 @@ export async function generateMetadata() { title: `${instanceName} — AI Gateway for Multi-Provider LLMs`, description: "OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.", + manifest: "/manifest.webmanifest", + applicationName: instanceName, + appleWebApp: { + capable: true, + title: instanceName, + statusBarStyle: "black-translucent", + }, + other: { + "mobile-web-app-capable": "yes", + }, icons: { - icon: customFaviconUrl ? "/api/settings/favicon" : "/favicon.svg", - apple: "/apple-touch-icon.svg", + icon: customFaviconUrl + ? "/api/settings/favicon" + : [ + { url: "/favicon.ico", sizes: "any" }, + { url: "/favicon.svg", type: "image/svg+xml" }, + { url: "/icon-512.png", type: "image/png", sizes: "512x512" }, + ], + apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], }, }; } @@ -67,6 +90,7 @@ export default async function RootLayout({ children }) { Skip to content + {children} diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000000..66c767a6a4 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,29 @@ +import type { MetadataRoute } from "next"; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: "OmniRoute", + short_name: "OmniRoute", + description: + "OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.", + start_url: "/", + scope: "/", + display: "fullscreen", + orientation: "any", + background_color: "#0b0f1a", + theme_color: "#0b0f1a", + icons: [ + { + src: "/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/apple-touch-icon.png", + sizes: "180x180", + type: "image/png", + }, + ], + }; +} diff --git a/src/shared/components/PwaRegister.tsx b/src/shared/components/PwaRegister.tsx new file mode 100644 index 0000000000..a737fb196a --- /dev/null +++ b/src/shared/components/PwaRegister.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useEffect } from "react"; + +export function PwaRegister() { + useEffect(() => { + if (typeof window === "undefined" || !("serviceWorker" in navigator)) { + return; + } + + navigator.serviceWorker.register("/sw.js").catch(() => { + // Ignore registration failures to avoid blocking app rendering. + }); + }, []); + + return null; +}