diff --git a/next.config.mjs b/next.config.mjs index 1c4efaa..2485c61 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,16 @@ /** @type {import('next').NextConfig} */ const nextConfig = { allowedDevOrigins: ["*.sats.day", "*.ngrok-free.app"], + experimental: { + // Since Next 15, page segments aren't reused across Link/router.push + // navigation by default (only layouts + true back/forward are). This + // lets the news list <-> article navigation reuse the client RSC cache + // instead of re-requesting the server on every tap. + staleTimes: { + dynamic: 60, + static: 900, + }, + }, async headers() { return [ { @@ -10,6 +20,16 @@ const nextConfig = { source: "/((?!_next/static|_next/image).*)", headers: [{ key: "Cache-Control", value: "no-store, must-revalidate" }], }, + { + // Service worker script: never cache (so updates ship immediately) + // and lock down what it's allowed to load. + source: "/sw.js", + headers: [ + { key: "Content-Type", value: "application/javascript; charset=utf-8" }, + { key: "Cache-Control", value: "no-cache, no-store, must-revalidate" }, + { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" }, + ], + }, ]; }, }; diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..eda67b1 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..7fb508e Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..34c0423 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-maskable-192.png b/public/icons/icon-maskable-192.png new file mode 100644 index 0000000..9954a55 Binary files /dev/null and b/public/icons/icon-maskable-192.png differ diff --git a/public/icons/icon-maskable-512.png b/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..43f0c67 Binary files /dev/null and b/public/icons/icon-maskable-512.png differ diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..fbd0725 --- /dev/null +++ b/public/offline.html @@ -0,0 +1,49 @@ + + +
+ + +You're offline. Check your connection and try again.
+ + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..ea7e69a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,71 @@ +const CACHE_VERSION = "v1"; +const STATIC_CACHE = `bd-static-${CACHE_VERSION}`; +const OFFLINE_URL = "/offline.html"; + +const PRECACHE_URLS = [OFFLINE_URL, "/icons/icon-192.png", "/icons/icon-512.png", "/favicon.ico"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(STATIC_CACHE) + .then((cache) => cache.addAll(PRECACHE_URLS)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((key) => key !== STATIC_CACHE).map((key) => caches.delete(key))) + ) + .then(() => self.clients.claim()) + ); +}); + +// Navigations (HTML documents) are intentionally network-only, never cached. +// This app forces Cache-Control: no-store on HTML routes to work around a +// Telegram Desktop WebView bug that serves stale pages (see next.config.mjs) +// — caching navigations here would reintroduce that exact bug. We only fall +// back to a static offline page when the network request fails outright. +async function handleNavigation(request) { + try { + return await fetch(request); + } catch { + const cache = await caches.open(STATIC_CACHE); + return (await cache.match(OFFLINE_URL)) ?? Response.error(); + } +} + +// Hashed Next.js build assets and local images are immutable — safe to +// cache-first indefinitely. +async function handleStaticAsset(request) { + const cache = await caches.open(STATIC_CACHE); + const cached = await cache.match(request); + if (cached) return cached; + + const response = await fetch(request); + if (response.ok) cache.put(request, response.clone()); + return response; +} + +self.addEventListener("fetch", (event) => { + const { request } = event; + if (request.method !== "GET") return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + // Never cache API responses — always fresh, subscription/wallet data included. + if (url.pathname.startsWith("/api/")) return; + + if (request.mode === "navigate") { + event.respondWith(handleNavigation(request)); + return; + } + + if (url.pathname.startsWith("/_next/static/") || url.pathname.startsWith("/icons/")) { + event.respondWith(handleStaticAsset(request)); + } +}); diff --git a/src/app/api/transaction/bot-history/route.ts b/src/app/api/transaction/bot-history/route.ts new file mode 100644 index 0000000..0046d98 --- /dev/null +++ b/src/app/api/transaction/bot-history/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from "next/server"; +import { headers } from "next/headers"; + +export async function GET(request: Request) { + try { + // Get authorization header + const headersList = await headers(); + const authorization = headersList.get("authorization"); + + if (!authorization) { + return NextResponse.json( + { + error: "Unauthorized", + message: "Authorization header is required", + }, + { status: 401 } + ); + } + + // Extract token from "Bearer