From ed384a88b1c49ae33ef271f427299efb136f01f1 Mon Sep 17 00:00:00 2001 From: Rahman Lawal Date: Thu, 28 May 2026 06:25:41 +0100 Subject: [PATCH 1/3] feat(frontend): improve accessibility and enable optimistic team permissions and health status features --- .../app/(authenticated)/dashboard/page.tsx | 4 +- .../src/app/(authenticated)/settings/page.tsx | 27 +- frontend/src/components/ApiHealthBadge.tsx | 97 +++-- .../src/components/UserPermissionsManager.tsx | 401 ++++++++++++++++++ frontend/src/components/WalletSelector.tsx | 2 +- 5 files changed, 489 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/UserPermissionsManager.tsx diff --git a/frontend/src/app/(authenticated)/dashboard/page.tsx b/frontend/src/app/(authenticated)/dashboard/page.tsx index bc124ec1..03107020 100644 --- a/frontend/src/app/(authenticated)/dashboard/page.tsx +++ b/frontend/src/app/(authenticated)/dashboard/page.tsx @@ -12,7 +12,7 @@ import { import FirstApiKeyModal from "@/components/FirstApiKeyModal"; import PaymentMetrics from "@/components/PaymentMetrics"; import RecentPayments from "@/components/RecentPayments"; -import WithdrawModal from "@/components/WithdrawModal"; +import WithdrawalModal from "@/components/WithdrawalModal"; import FirstPaymentCelebration from "@/components/FirstPaymentCelebration"; export default function DashboardPage() { @@ -133,7 +133,7 @@ export default function DashboardPage() { setIsFirstKeyModalOpen(false)} /> - setIsWithdrawOpen(false)} /> + setIsWithdrawOpen(false)} /> ); } diff --git a/frontend/src/app/(authenticated)/settings/page.tsx b/frontend/src/app/(authenticated)/settings/page.tsx index 0e663f53..4f5ad001 100644 --- a/frontend/src/app/(authenticated)/settings/page.tsx +++ b/frontend/src/app/(authenticated)/settings/page.tsx @@ -16,6 +16,7 @@ import { useDisplayPreferences } from "@/lib/display-preferences"; import WebhookHealthIndicator from "@/components/WebhookHealthIndicator"; import DangerZone from "@/components/DangerZone"; import { EmailReceiptPreview } from "@/components/EmailReceiptPreview"; +import UserPermissionsManager from "@/components/UserPermissionsManager"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; const HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; @@ -26,7 +27,7 @@ const DEFAULT_BRANDING = { logo_url: null as string | null, }; -type SettingsTab = "api" | "branding" | "display" | "webhooks" | "danger"; +type SettingsTab = "api" | "branding" | "display" | "webhooks" | "permissions" | "danger"; interface WebhookDomainVerification { status: "verified" | "unverified"; @@ -198,6 +199,25 @@ const NAV_ITEMS: { ), }, + { + id: "permissions", + label: "Permissions", + icon: ( + + + + ), + }, { id: "danger", label: "Danger Zone", @@ -1104,6 +1124,11 @@ export default function SettingsPage() { )} + {/* Permissions Tab */} + {activeTab === "permissions" && ( + + )} + {/* Danger Tab */} {activeTab === "danger" && (
diff --git a/frontend/src/components/ApiHealthBadge.tsx b/frontend/src/components/ApiHealthBadge.tsx index ed8b3c7f..875b0b64 100644 --- a/frontend/src/components/ApiHealthBadge.tsx +++ b/frontend/src/components/ApiHealthBadge.tsx @@ -8,53 +8,61 @@ export default function ApiHealthBadge() { const [status, setStatus] = useState("loading"); const [errorMsg, setErrorMsg] = useState(null); - useEffect(() => { - let mounted = true; - const checkHealth = async () => { - try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const res = await fetch(`${apiUrl}/health`, { cache: "no-store" }); - const data = await res.json().catch(() => ({})); + const checkHealth = async () => { + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; + const res = await fetch(`${apiUrl}/health`, { cache: "no-store" }); + const data = await res.json().catch(() => ({})); - if (mounted) { - const services = data?.services ?? {}; - const dbOk = services.database === "ok"; - const horizonOk = services.horizon === "ok"; - const apiReachable = true; // If we got an HTTP response, backend is reachable. - const healthy = apiReachable; + const services = data?.services ?? {}; + const dbOk = services.database === "ok"; + const horizonOk = services.horizon === "ok"; + const apiReachable = true; // If we got an HTTP response, backend is reachable. + const healthy = apiReachable; - if (healthy) { - setStatus("healthy"); - const missing: string[] = []; - if (!dbOk) missing.push("database"); - if (!horizonOk) missing.push("horizon"); - setErrorMsg( - missing.length > 0 - ? `Degraded dependency: ${missing.join(" + ")} unavailable` - : null, - ); - } else { - setStatus("error"); - setErrorMsg(data?.error || "Backend unavailable"); - } - } - } catch { - if (mounted) { - setStatus("error"); - setErrorMsg("API Unreachable"); - } + if (healthy) { + setStatus("healthy"); + const missing: string[] = []; + if (!dbOk) missing.push("database"); + if (!horizonOk) missing.push("horizon"); + setErrorMsg( + missing.length > 0 + ? `Degraded dependency: ${missing.join(" + ")} unavailable` + : null, + ); + } else { + setStatus("error"); + setErrorMsg(data?.error || "Backend unavailable"); } - }; + } catch { + setStatus("error"); + setErrorMsg("API Unreachable"); + } + }; + useEffect(() => { checkHealth(); // Re-check every 60 seconds const interval = setInterval(checkHealth, 60000); return () => { - mounted = false; clearInterval(interval); }; }, []); + const handleRefresh = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (status === "loading") return; + + // Optimistic Update: Immediately transition to visual checking/loading state + setStatus("loading"); + setErrorMsg(null); + + // satisfy with a micro-delay so the animation feels responsive and interactive + await new Promise((resolve) => setTimeout(resolve, 400)); + await checkHealth(); + }; + const config = { loading: { color: "bg-[#E8E8E8]", @@ -77,7 +85,15 @@ export default function ApiHealthBadge() { }[status]; return ( -
+ ); } + diff --git a/frontend/src/components/UserPermissionsManager.tsx b/frontend/src/components/UserPermissionsManager.tsx new file mode 100644 index 00000000..63efb4cb --- /dev/null +++ b/frontend/src/components/UserPermissionsManager.tsx @@ -0,0 +1,401 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "sonner"; + +export type TeamRole = "Owner" | "Administrator" | "Developer" | "Support" | "Viewer"; + +export interface TeamMember { + id: string; + email: string; + role: TeamRole; + status: "Active" | "Invited"; + joinedAt: string; +} + +const DEFAULT_MEMBERS: TeamMember[] = [ + { + id: "mem_1", + email: "owner@pluto.storage", + role: "Owner", + status: "Active", + joinedAt: "2026-01-10", + }, + { + id: "mem_2", + email: "lead-dev@pluto.storage", + role: "Developer", + status: "Active", + joinedAt: "2026-03-15", + }, + { + id: "mem_3", + email: "support-agent@pluto.storage", + role: "Support", + status: "Invited", + joinedAt: "2026-05-27", + }, +]; + +const ROLE_DESCRIPTIONS: Record = { + Owner: "Full access to billing, key rotation, database, and all settings.", + Administrator: "Can manage all settings, webhooks, and team members except key rotation.", + Developer: "Can read/write API keys, view payment logs, and test in sandbox.", + Support: "Can view payments, access dashboard charts, and process refunds.", + Viewer: "Read-only access to dashboard statistics and payment logs.", +}; + +export default function UserPermissionsManager() { + const [members, setMembers] = useState([]); + const [inviteEmail, setInviteEmail] = useState(""); + const [inviteRole, setInviteRole] = useState("Developer"); + const [isInviting, setIsInviting] = useState(false); + const [filterRole, setFilterRole] = useState("All"); + + // Load from localStorage or default on mount + useEffect(() => { + const saved = localStorage.getItem("pluto_team_members"); + if (saved) { + try { + setMembers(JSON.parse(saved)); + } catch { + setMembers(DEFAULT_MEMBERS); + } + } else { + setMembers(DEFAULT_MEMBERS); + localStorage.setItem("pluto_team_members", JSON.stringify(DEFAULT_MEMBERS)); + } + }, []); + + const saveToStorage = (updatedList: TeamMember[]) => { + localStorage.setItem("pluto_team_members", JSON.stringify(updatedList)); + }; + + const handleInvite = async (e: React.FormEvent) => { + e.preventDefault(); + if (!inviteEmail.trim()) { + toast.error("Please enter a valid email address."); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(inviteEmail.trim())) { + toast.error("Invalid email address format."); + return; + } + + if (members.some((m) => m.email.toLowerCase() === inviteEmail.trim().toLowerCase())) { + toast.error("A team member with this email already exists."); + return; + } + + setIsInviting(true); + + const newMember: TeamMember = { + id: `mem_${Date.now()}`, + email: inviteEmail.trim().toLowerCase(), + role: inviteRole, + status: "Invited", + joinedAt: new Date().toISOString().split("T")[0], + }; + + // Keep reference of previous list for rollback if API fails + const previousMembers = [...members]; + + // Optimistic Update: Immediately add the new member to the list + const optimisticallyUpdatedMembers = [...members, newMember]; + setMembers(optimisticallyUpdatedMembers); + setInviteEmail(""); + + try { + // Simulate API network request latency + await new Promise((resolve, reject) => { + setTimeout(() => { + // 95% success rate for simulation + if (Math.random() > 0.05) { + resolve(true); + } else { + reject(new Error("API server timed out")); + } + }, 800); + }); + + saveToStorage(optimisticallyUpdatedMembers); + toast.success(`Successfully invited ${newMember.email} as ${newMember.role}`); + } catch (err: unknown) { + // Revert/Rollback on failure + setMembers(previousMembers); + const msg = err instanceof Error ? err.message : "Failed to invite user"; + toast.error(`Error: ${msg}. Reverted state.`); + } finally { + setIsInviting(false); + } + }; + + const handleRoleChange = async (memberId: string, newRole: TeamRole) => { + const previousMembers = [...members]; + + // Optimistic Update: Immediately update role in list state + const optimisticallyUpdatedMembers = members.map((m) => + m.id === memberId ? { ...m, role: newRole } : m + ); + setMembers(optimisticallyUpdatedMembers); + + try { + // Simulate API latency + await new Promise((resolve) => setTimeout(resolve, 500)); + saveToStorage(optimisticallyUpdatedMembers); + toast.success("Team member role successfully updated."); + } catch { + // Revert/Rollback on failure + setMembers(previousMembers); + toast.error("Failed to update role. Reverted state."); + } + }; + + const handleRevoke = async (memberId: string) => { + const targetMember = members.find((m) => m.id === memberId); + if (!targetMember) return; + + if (targetMember.role === "Owner") { + toast.error("The account Owner's permissions cannot be revoked."); + return; + } + + if (!confirm(`Are you sure you want to revoke access for ${targetMember.email}?`)) { + return; + } + + const previousMembers = [...members]; + + // Optimistic Update: Immediately remove member from list state + const optimisticallyUpdatedMembers = members.filter((m) => m.id !== memberId); + setMembers(optimisticallyUpdatedMembers); + + try { + // Simulate API latency + await new Promise((resolve) => setTimeout(resolve, 600)); + saveToStorage(optimisticallyUpdatedMembers); + toast.success(`Revoked access for ${targetMember.email}`); + } catch { + // Revert/Rollback on failure + setMembers(previousMembers); + toast.error("Failed to revoke access. Reverted state."); + } + }; + + const filteredMembers = filterRole === "All" + ? members + : members.filter((m) => m.role === filterRole); + + return ( +
+ {/* Invite Member form */} +
+
+

+ Invite Team Member +

+

+ Add team members and define their exact workspace accessibility level. +

+
+ +
+
+ + setInviteEmail(e.target.value)} + disabled={isInviting} + className="h-11 rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-4 text-sm text-[#0A0A0A] placeholder-slate-400 focus:border-[#4a6fa5] focus:bg-white outline-none transition-all disabled:opacity-50" + /> +
+ +
+ + +
+ + +
+ + {/* Role description box */} +
+

+ Role Access Level: {inviteRole} +

+

+ {ROLE_DESCRIPTIONS[inviteRole]} +

+
+
+ + {/* Member List section */} +
+
+
+

+ Active Workspace Team +

+

+ {filteredMembers.length} active or pending members on your merchant account. +

+
+ + {/* Role Filter */} +
+ + +
+
+ + {/* Table of Members */} +
+ + + + + + + + + + + + {filteredMembers.map((member) => ( + + + + + + + + + + ))} + + + {filteredMembers.length === 0 && ( + + + + )} + +
Member / EmailWorkspace RoleConnectionAction
+
+ {member.email} + + Joined on {member.joinedAt} + +
+
+ {member.role === "Owner" ? ( + + Owner + + ) : ( + + )} + +
+ + + {member.status} + +
+
+ {member.role !== "Owner" && ( + + )} +
+ No team members match this filter. +
+
+
+
+ ); +} diff --git a/frontend/src/components/WalletSelector.tsx b/frontend/src/components/WalletSelector.tsx index f81eabfe..b38dd41a 100644 --- a/frontend/src/components/WalletSelector.tsx +++ b/frontend/src/components/WalletSelector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { useWallet } from "@/lib/wallet-context"; import { connectWalletConnect } from "@/lib/wallet-walletconnect"; From d15d1eb8e7f8bbe3e37f5b61f8bd9a51b552dc1d Mon Sep 17 00:00:00 2001 From: Rahman Lawal Date: Thu, 28 May 2026 08:01:37 +0100 Subject: [PATCH 2/3] fix issues --- frontend/public/sw.js | 1 + frontend/public/workbox-3c9d0171.js | 1 + 2 files changed, 2 insertions(+) create mode 100644 frontend/public/sw.js create mode 100644 frontend/public/workbox-3c9d0171.js diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 00000000..b0a83b3a --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1 @@ +if(!self.define){let e,a={};const s=(s,i)=>(s=new URL(s+".js",i).href,a[s]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=a,document.head.appendChild(e)}else e=s,importScripts(s),a()}).then(()=>{let e=a[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(i,t)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(a[n])return;let c={};const r=e=>s(e,n),l={module:{uri:n},exports:c,require:r};a[n]=Promise.all(i.map(e=>l[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-3c9d0171"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/IrbJlNOlrbiwafUW76FLP/_buildManifest.js",revision:"dbaa58eadfdba9b6d2611d290c181400"},{url:"/_next/static/IrbJlNOlrbiwafUW76FLP/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/1142-476b30fbe78ee574.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/1163-8f4f221fddcadcd4.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/1734-f3d65668ab2bf212.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/2184-c2a05305f2998530.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/2582-20ebbdb517607ad4.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/2783-3a3b878d73cd7ab5.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/4817-d47209aa9094ab5f.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/598a0686-2cbd9a6e31f7d0a7.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/5990-d691fce6db5f3190.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/6626-168bed810d12cd0a.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/6737-952344274686ec47.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/69-80b836ecf6220704.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/70dae772-bb747cc25b7dc8ce.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/7495-d00d03b0d10ed585.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/7547-a49e17935fbdac33.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/769-4dfe60bc40cff307.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/7cdb0f48-a9157f21c45b683a.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/8192.f7290e3f28b197f9.js",revision:"f7290e3f28b197f9"},{url:"/_next/static/chunks/8328-794146944dcc3390.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/8619-c6550057863a0a79.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/8634-22cb42099ef0788a.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/9434-a07c110aa3424c48.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/api-keys/page-7fde09879238bfb0.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/create/page-414380d49ca96f65.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/dashboard/create/page-ae556c863c1462fb.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/dashboard/dev-tools/page-7c6454e44291da80.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/dashboard/page-c864a13a034d3692.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/layout-c7d480a1128e3c1d.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/payment-history/page-efb2c55cf8dfd4ab.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/payments/page-51cd7e22029f65b2.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/settings/page-2afad719a2f7103c.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(authenticated)/webhook-logs/page-dbb2aadcb2fc1684.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/docs/%5Bslug%5D/page-3d413256a472fa92.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/docs/layout-c78458ff8f3c7414.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/docs/page-21d07638fe5ff6e4.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/forgot-password/page-ce7e566d7cf580f9.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/layout-95f70cc3b12af521.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/login/page-059b3310d5e11969.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/page-6cdcf56d0764364d.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/register/page-0014dd705f3920d6.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/(public)/vrt/page-3313ec527cde5b7d.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/_not-found/page-7245cfafeeec7d22.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/error-2b0b7dfd93b63df3.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/global-error-f9acaf369a64c638.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/layout-581b9a7216798c69.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/not-found-d2a6d0bad9d0169a.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/offline/page-cf4ba739bf06d3ab.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/pay/%5Bid%5D/cancelled/page-fd9c3c03a994721d.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/pay/%5Bid%5D/layout-ac7b75e45100ae3f.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/app/pay/%5Bid%5D/page-26b19710fe3584c6.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/framework-63a5d844a3662ade.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/main-3b3464bd00fe5694.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/main-app-9071da9de46aaa77.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/pages/_app-d691402d67feab84.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/pages/_error-c691f01e93769e09.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-c77209976e109607.js",revision:"IrbJlNOlrbiwafUW76FLP"},{url:"/_next/static/css/00c0de1a59eba2f6.css",revision:"00c0de1a59eba2f6"},{url:"/_next/static/css/20720a659c31249d.css",revision:"20720a659c31249d"},{url:"/_next/static/css/fa3dcbc1406e48a4.css",revision:"fa3dcbc1406e48a4"},{url:"/_next/static/media/281dae1e814de8c6-s.woff2",revision:"2e8643e59f8dcca240efa656d5965385"},{url:"/_next/static/media/3c70c5716f1730b3-s.woff2",revision:"f5ca848fdd0823d0e166a4eb4164bc5a"},{url:"/_next/static/media/77fb5eec12c66d49-s.woff2",revision:"0af756a4168d80d59022d8bedb3305a9"},{url:"/_next/static/media/806de4d605d3ad01-s.p.woff2",revision:"55408a774dfe38eb61f842b03ef9e24a"},{url:"/_next/static/media/ae822095a172cc5c-s.woff2",revision:"21132e0765e0538063d507723efe0d3d"},{url:"/_next/static/media/d4fbdff1b926f9f7-s.woff2",revision:"3daaf2b411f39444d4ff1bb3b53a097f"},{url:"/_next/static/media/d8c14dc5fcaf3a63-s.p.woff2",revision:"c933a93e9a8769d2ebbe057d338705f4"},{url:"/_next/static/media/e1bfc245270dd1fc-s.woff2",revision:"915e330084148aa671bbcf4d7f4751ac"},{url:"/_next/static/media/fc727f226c737876-s.p.woff2",revision:"d62e75a5cd2d7406d1c855ae5aac62d3"},{url:"/icons/icon-192x192.png",revision:"e84691a47a7e3edcf6f9109036b52a12"},{url:"/icons/icon-512x512.png",revision:"e84691a47a7e3edcf6f9109036b52a12"},{url:"/manifest.json",revision:"a0a82257f255bd11ad6c5c683311857e"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e,url:{pathname:a}})=>!(!e||a.startsWith("/api/auth/callback")||!a.startsWith("/api/")),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:a},sameOrigin:s})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&s&&!a.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:a},sameOrigin:s})=>"1"===e.headers.get("RSC")&&s&&!a.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:{pathname:e},sameOrigin:a})=>a&&!e.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e})=>!e,new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); diff --git a/frontend/public/workbox-3c9d0171.js b/frontend/public/workbox-3c9d0171.js new file mode 100644 index 00000000..8d64e054 --- /dev/null +++ b/frontend/public/workbox-3c9d0171.js @@ -0,0 +1 @@ +define(["exports"],function(t){"use strict";try{self["workbox:core:7.0.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.0.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class i extends r{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class a{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let a=i&&i.handler;const o=t.method;if(!a&&this.i.has(o)&&(a=this.i.get(o)),!a)return;let c;try{c=a.handle({url:s,request:t,event:e,params:r})}catch(t){c=Promise.reject(t)}const h=i&&i.catchHandler;return c instanceof Promise&&(this.o||h)&&(c=c.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:r})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const r=this.t.get(s.method)||[];for(const i of r){let r;const a=i.match({url:t,sameOrigin:e,request:s,event:n});if(a)return r=a,(Array.isArray(r)&&0===r.length||a.constructor===Object&&0===Object.keys(a).length||"boolean"==typeof a)&&(r=void 0),{route:i,params:r}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let o;const c=()=>(o||(o=new a,o.addFetchListener(),o.addCacheListener()),o);function h(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new r(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)a=new i(t,e,n);else if("function"==typeof t)a=new r(t,e,n);else{if(!(t instanceof r))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}return c().registerRoute(a),a}try{self["workbox:strategies:7.0.0"]&&_()}catch(t){}const u={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null},l={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},f=t=>[l.prefix,t,l.suffix].filter(t=>t&&t.length>0).join("-"),w=t=>t||f(l.precache),d=t=>t||f(l.runtime);function p(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class y{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const g=new Set;function m(t){return"string"==typeof t?new Request(t):t}class v{constructor(t,e){this.h={},Object.assign(this,e),this.event=e.event,this.u=t,this.l=new y,this.p=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.l.promise)}async fetch(t){const{event:e}=this;let n=m(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const r=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const i=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.u.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:i,response:t});return t}catch(t){throw r&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:r.clone(),request:i.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=m(t);let s;const{cacheName:n,matchOptions:r}=this.u,i=await this.getCacheKey(e,"read"),a=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(i,a);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:r,cachedResponse:s,request:i,event:this.event})||void 0;return s}async cachePut(t,e){const n=m(t);var r;await(r=0,new Promise(t=>setTimeout(t,r)));const i=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(a=i.url,new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var a;const o=await this.R(e);if(!o)return!1;const{cacheName:c,matchOptions:h}=this.u,u=await self.caches.open(c),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const r=p(e.url,s);if(e.url===r)return t.match(e,n);const i=Object.assign(Object.assign({},n),{ignoreSearch:!0}),a=await t.keys(e,i);for(const e of a)if(r===p(e.url,s))return t.match(e,n)}(u,i.clone(),["__WB_REVISION__"],h):null;try{await u.put(i,l?o.clone():o)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of g)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:c,oldResponse:f,newResponse:o.clone(),request:i,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.h[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await t({mode:e,request:n,event:this.event,params:this.params}));this.h[s]=n}return this.h[s]}hasCallback(t){for(const e of this.u.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.u.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const r=Object.assign(Object.assign({},n),{state:s});return e[t](r)};yield n}}waitUntil(t){return this.p.push(t),t}async doneWaiting(){let t;for(;t=this.p.shift();)await t}destroy(){this.l.resolve(null)}async R(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class R{constructor(t={}){this.cacheName=d(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,r=new v(this,{event:e,request:s,params:n}),i=this.q(r,s,e);return[i,this.D(i,r,s,e)]}async q(t,e,n){let r;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(r=await this.U(e,t),!r||"error"===r.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const i of t.iterateCallbacks("handlerDidError"))if(r=await i({error:s,event:n,request:e}),r)break;if(!r)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))r=await s({event:n,request:e,response:r});return r}async D(t,e,s,n){let r,i;try{r=await t}catch(i){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await e.doneWaiting()}catch(t){t instanceof Error&&(i=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:i}),e.destroy(),i)throw i}}function b(t){t.then(()=>{})}function q(){return q=Object.assign?Object.assign.bind():function(t){for(var e=1;e(t[e]=s,!0),has:(t,e)=>t instanceof IDBTransaction&&("done"===e||"store"===e)||e in t};function O(t){return t!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(U||(U=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(t)?function(...e){return t.apply(B(this),e),k(x.get(this))}:function(...e){return k(t.apply(B(this),e))}:function(e,...s){const n=t.call(B(this),e,...s);return I.set(n,e.sort?e.sort():[e]),k(n)}}function T(t){return"function"==typeof t?O(t):(t instanceof IDBTransaction&&function(t){if(L.has(t))return;const e=new Promise((e,s)=>{const n=()=>{t.removeEventListener("complete",r),t.removeEventListener("error",i),t.removeEventListener("abort",i)},r=()=>{e(),n()},i=()=>{s(t.error||new DOMException("AbortError","AbortError")),n()};t.addEventListener("complete",r),t.addEventListener("error",i),t.addEventListener("abort",i)});L.set(t,e)}(t),e=t,(D||(D=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some(t=>e instanceof t)?new Proxy(t,N):t);var e}function k(t){if(t instanceof IDBRequest)return function(t){const e=new Promise((e,s)=>{const n=()=>{t.removeEventListener("success",r),t.removeEventListener("error",i)},r=()=>{e(k(t.result)),n()},i=()=>{s(t.error),n()};t.addEventListener("success",r),t.addEventListener("error",i)});return e.then(e=>{e instanceof IDBCursor&&x.set(e,t)}).catch(()=>{}),E.set(e,t),e}(t);if(C.has(t))return C.get(t);const e=T(t);return e!==t&&(C.set(t,e),E.set(e,t)),e}const B=t=>E.get(t);const P=["get","getKey","getAll","getAllKeys","count"],M=["put","add","delete","clear"],W=new Map;function j(t,e){if(!(t instanceof IDBDatabase)||e in t||"string"!=typeof e)return;if(W.get(e))return W.get(e);const s=e.replace(/FromIndex$/,""),n=e!==s,r=M.includes(s);if(!(s in(n?IDBIndex:IDBObjectStore).prototype)||!r&&!P.includes(s))return;const i=async function(t,...e){const i=this.transaction(t,r?"readwrite":"readonly");let a=i.store;return n&&(a=a.index(e.shift())),(await Promise.all([a[s](...e),r&&i.done]))[0]};return W.set(e,i),i}N=(t=>q({},t,{get:(e,s,n)=>j(e,s)||t.get(e,s,n),has:(e,s)=>!!j(e,s)||t.has(e,s)}))(N);try{self["workbox:expiration:7.0.0"]&&_()}catch(t){}const S="cache-entries",K=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class A{constructor(t){this._=null,this.L=t}I(t){const e=t.createObjectStore(S,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}C(t){this.I(t),this.L&&function(t,{blocked:e}={}){const s=indexedDB.deleteDatabase(t);e&&s.addEventListener("blocked",t=>e(t.oldVersion,t)),k(s).then(()=>{})}(this.L)}async setTimestamp(t,e){const s={url:t=K(t),timestamp:e,cacheName:this.L,id:this.N(t)},n=(await this.getDb()).transaction(S,"readwrite",{durability:"relaxed"});await n.store.put(s),await n.done}async getTimestamp(t){const e=await this.getDb(),s=await e.get(S,this.N(t));return null==s?void 0:s.timestamp}async expireEntries(t,e){const s=await this.getDb();let n=await s.transaction(S).store.index("timestamp").openCursor(null,"prev");const r=[];let i=0;for(;n;){const s=n.value;s.cacheName===this.L&&(t&&s.timestamp=e?r.push(n.value):i++),n=await n.continue()}const a=[];for(const t of r)await s.delete(S,t.id),a.push(t.url);return a}N(t){return this.L+"|"+K(t)}async getDb(){return this._||(this._=await function(t,e,{blocked:s,upgrade:n,blocking:r,terminated:i}={}){const a=indexedDB.open(t,e),o=k(a);return n&&a.addEventListener("upgradeneeded",t=>{n(k(a.result),t.oldVersion,t.newVersion,k(a.transaction),t)}),s&&a.addEventListener("blocked",t=>s(t.oldVersion,t.newVersion,t)),o.then(t=>{i&&t.addEventListener("close",()=>i()),r&&t.addEventListener("versionchange",t=>r(t.oldVersion,t.newVersion,t))}).catch(()=>{}),o}("workbox-expiration",1,{upgrade:this.C.bind(this)})),this._}}class F{constructor(t,e={}){this.O=!1,this.T=!1,this.k=e.maxEntries,this.B=e.maxAgeSeconds,this.P=e.matchOptions,this.L=t,this.M=new A(t)}async expireEntries(){if(this.O)return void(this.T=!0);this.O=!0;const t=this.B?Date.now()-1e3*this.B:0,e=await this.M.expireEntries(t,this.k),s=await self.caches.open(this.L);for(const t of e)await s.delete(t,this.P);this.O=!1,this.T&&(this.T=!1,b(this.expireEntries()))}async updateTimestamp(t){await this.M.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.B){const e=await this.M.getTimestamp(t),s=Date.now()-1e3*this.B;return void 0===e||er||e&&e<0)throw new s("range-not-satisfiable",{size:r,end:n,start:e});let i,a;return void 0!==e&&void 0!==n?(i=e,a=n+1):void 0!==e&&void 0===n?(i=e,a=r):void 0!==n&&void 0===e&&(i=r-n,a=r),{start:i,end:a}}(i,r.start,r.end),o=i.slice(a.start,a.end),c=o.size,h=new Response(o,{status:206,statusText:"Partial Content",headers:e.headers});return h.headers.set("Content-Length",String(c)),h.headers.set("Content-Range",`bytes ${a.start}-${a.end-1}/${i.size}`),h}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}function $(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.0.0"]&&_()}catch(t){}function z(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const r=new URL(n,location.href),i=new URL(n,location.href);return r.searchParams.set("__WB_REVISION__",e),{cacheKey:r.href,url:i.href}}class G{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class V{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.W.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.W=t}}let J,Q;async function X(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const r=t.clone(),i={headers:new Headers(r.headers),status:r.status,statusText:r.statusText},a=e?e(i):i,o=function(){if(void 0===J){const t=new Response("");if("body"in t)try{new Response(t.body),J=!0}catch(t){J=!1}J=!1}return J}()?r.body:await r.blob();return new Response(o,a)}class Y extends R{constructor(t={}){t.cacheName=w(t.cacheName),super(t),this.j=!1!==t.fallbackToNetwork,this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin)}async U(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.S(t,e):await this.K(t,e))}async K(t,e){let n;const r=e.params||{};if(!this.j)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=r.integrity,i=t.integrity,a=!i||i===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?i||s:void 0})),s&&a&&"no-cors"!==t.mode&&(this.A(),await e.cachePut(t,n.clone()))}return n}async S(t,e){this.A();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}A(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==Y.copyRedirectedCacheableResponsesPlugin&&(n===Y.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(Y.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}Y.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},Y.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await X(t):t};class Z{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.F=new Map,this.H=new Map,this.$=new Map,this.u=new Y({cacheName:w(t),plugins:[...e,new V({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.u}precache(t){this.addToCacheList(t),this.G||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.G=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:r}=z(n),i="string"!=typeof n&&n.revision?"reload":"default";if(this.F.has(r)&&this.F.get(r)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.F.get(r),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.$.has(t)&&this.$.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:r});this.$.set(t,n.integrity)}if(this.F.set(r,t),this.H.set(r,i),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return $(t,async()=>{const e=new G;this.strategy.plugins.push(e);for(const[e,s]of this.F){const n=this.$.get(s),r=this.H.get(e),i=new Request(e,{integrity:n,cache:r,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:i,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}})}activate(t){return $(t,async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.F.values()),n=[];for(const r of e)s.has(r.url)||(await t.delete(r),n.push(r.url));return{deletedURLs:n}})}getURLsToCacheKeys(){return this.F}getCachedURLs(){return[...this.F.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.F.get(e.href)}getIntegrityForCacheKey(t){return this.$.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}const tt=()=>(Q||(Q=new Z),Q);class et extends r{constructor(t,e){super(({request:s})=>{const n=t.getURLsToCacheKeys();for(const r of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:r}={}){const i=new URL(t,location.href);i.hash="",yield i.href;const a=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some(t=>t.test(s))&&t.searchParams.delete(s);return t}(i,e);if(yield a.href,s&&a.pathname.endsWith("/")){const t=new URL(a.href);t.pathname+=s,yield t.href}if(n){const t=new URL(a.href);t.pathname+=".html",yield t.href}if(r){const t=r({url:i});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(r);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}},t.strategy)}}t.CacheFirst=class extends R{async U(t,e){let n,r=await e.cacheMatch(t);if(!r)try{r=await e.fetchAndCachePut(t)}catch(t){t instanceof Error&&(n=t)}if(!r)throw new s("no-response",{url:t.url,error:n});return r}},t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:e,cacheName:s,cachedResponse:n})=>{if(!n)return null;const r=this.V(n),i=this.J(s);b(i.expireEntries());const a=i.updateTimestamp(e.url);if(t)try{t.waitUntil(a)}catch(t){}return r?n:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.J(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.X=t,this.B=t.maxAgeSeconds,this.Y=new Map,t.purgeOnQuotaError&&function(t){g.add(t)}(()=>this.deleteCacheAndMetadata())}J(t){if(t===d())throw new s("expire-custom-caches-only");let e=this.Y.get(t);return e||(e=new F(t,this.X),this.Y.set(t,e)),e}V(t){if(!this.B)return!0;const e=this.Z(t);if(null===e)return!0;return e>=Date.now()-1e3*this.B}Z(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.Y)await self.caches.delete(t),await e.delete();this.Y=new Map}},t.NetworkFirst=class extends R{constructor(t={}){super(t),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(u),this.tt=t.networkTimeoutSeconds||0}async U(t,e){const n=[],r=[];let i;if(this.tt){const{id:s,promise:a}=this.et({request:t,logs:n,handler:e});i=s,r.push(a)}const a=this.st({timeoutId:i,request:t,logs:n,handler:e});r.push(a);const o=await e.waitUntil((async()=>await e.waitUntil(Promise.race(r))||await a)());if(!o)throw new s("no-response",{url:t.url});return o}et({request:t,logs:e,handler:s}){let n;return{promise:new Promise(e=>{n=setTimeout(async()=>{e(await s.cacheMatch(t))},1e3*this.tt)}),id:n}}async st({timeoutId:t,request:e,logs:s,handler:n}){let r,i;try{i=await n.fetchAndCachePut(e)}catch(t){t instanceof Error&&(r=t)}return t&&clearTimeout(t),!r&&i||(i=await n.cacheMatch(e)),i}},t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await H(t,e):e}},t.StaleWhileRevalidate=class extends R{constructor(t={}){super(t),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(u)}async U(t,e){const n=e.fetchAndCachePut(t).catch(()=>{});e.waitUntil(n);let r,i=await e.cacheMatch(t);if(i);else try{i=await n}catch(t){t instanceof Error&&(r=t)}if(!i)throw new s("no-response",{url:t.url,error:r});return i}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",t=>{const e=w();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter(s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t);return await Promise.all(s.map(t=>self.caches.delete(t))),s})(e).then(t=>{}))})},t.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},t.precacheAndRoute=function(t,e){!function(t){tt().precache(t)}(t),function(t){const e=tt();h(new et(e,t))}(e)},t.registerRoute=h}); From e93a87126fb615a19ae0b372ddc516a768e680bb Mon Sep 17 00:00:00 2001 From: Rahman Lawal Date: Thu, 28 May 2026 08:03:23 +0100 Subject: [PATCH 3/3] docs: add detailed pull request description document --- PULL_REQUEST_DESCRIPTION.md | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 PULL_REQUEST_DESCRIPTION.md diff --git a/PULL_REQUEST_DESCRIPTION.md b/PULL_REQUEST_DESCRIPTION.md new file mode 100644 index 00000000..bb77565d --- /dev/null +++ b/PULL_REQUEST_DESCRIPTION.md @@ -0,0 +1,50 @@ +# Pull Request Description + +## Title +`feat(frontend): improve accessibility, enable optimistic user permissions, and enhance network status dashboard controls` + +## Overview +This PR introduces frontend accessibility enhancements, state refactoring, and polished visual micro-interactions across the Pluto dashboard framework. It successfully addresses four frontend tasks (#821, #822, #823, and #824) while ensuring total system compile stability. + +--- + +## Detailed Changes + +### 1. Network Status Indicator (`ApiHealthBadge.tsx`) +* **Interactive Controls**: Refactored the container from a passive `div` to a semantic `