diff --git a/api/_upstash-cache.js b/api/_upstash-cache.js index 6e3557a6b..32819551c 100644 --- a/api/_upstash-cache.js +++ b/api/_upstash-cache.js @@ -1,51 +1,144 @@ -import { Redis } from '@upstash/redis'; +const isSidecar = (process.env.LOCAL_API_MODE || '').includes('sidecar'); +// ── In-memory cache (desktop/sidecar) ── +const mem = new Map(); +let persistPath = null; +let persistTimer = null; +let loaded = false; + +async function ensureDesktopCache() { + if (loaded) return; + loaded = true; + try { + const { join } = await import('node:path'); + const { readFileSync } = await import('node:fs'); + const dir = process.env.LOCAL_API_RESOURCE_DIR || '.'; + persistPath = join(dir, 'api-cache.json'); + const data = JSON.parse(readFileSync(persistPath, 'utf8')); + const now = Date.now(); + for (const [k, entry] of Object.entries(data)) { + if (entry.expiresAt > now) mem.set(k, entry); + } + console.log(`[Cache] Loaded ${mem.size} entries from disk`); + } catch { + // File doesn't exist yet + } + setInterval(() => { + const now = Date.now(); + for (const [k, v] of mem) { + if (v.expiresAt <= now) mem.delete(k); + } + }, 60_000).unref?.(); +} + +function debouncedPersist() { + if (!persistPath) return; + clearTimeout(persistTimer); + persistTimer = setTimeout(async () => { + try { + const { writeFileSync, renameSync } = await import('node:fs'); + const tmp = persistPath + '.tmp'; + writeFileSync(tmp, JSON.stringify(Object.fromEntries(mem))); + renameSync(tmp, persistPath); + } catch (err) { + console.warn('[Cache] Persist error:', err.message); + } + }, 2000); + if (persistTimer?.unref) persistTimer.unref(); +} + +// ── Redis (cloud/Vercel) ── +let RedisClass = null; let redis = null; let redisInitFailed = false; -export function getRedis() { +export async function getRedis() { + if (isSidecar) return null; if (redis) return redis; if (redisInitFailed) return null; const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (!url || !token) { + if (!url || !token) return null; + + try { + if (!RedisClass) { + const mod = await import('@upstash/redis'); + RedisClass = mod.Redis; + } + redis = new RedisClass({ url, token }); + return redis; + } catch (err) { + redisInitFailed = true; + console.warn('[Cache] Redis init failed:', err.message); return null; } - - try { - redis = new Redis({ url, token }); - } catch (error) { - redisInitFailed = true; - console.warn('[Cache] Redis init failed:', error.message); - } - - return redis; } +// ── Shared API ── + export async function getCachedJson(key) { - const redisClient = getRedis(); - if (!redisClient) return null; + if (isSidecar) { + await ensureDesktopCache(); + const entry = mem.get(key); + if (!entry) return null; + if (entry.expiresAt <= Date.now()) { + mem.delete(key); + return null; + } + return entry.value; + } + + const r = await getRedis(); + if (!r) return null; try { - return await redisClient.get(key); - } catch (error) { - console.warn('[Cache] Read failed:', error.message); + return await r.get(key); + } catch (err) { + console.warn('[Cache] Read failed:', err.message); return null; } } export async function setCachedJson(key, value, ttlSeconds) { - const redisClient = getRedis(); - if (!redisClient) return false; - try { - await redisClient.set(key, value, { ex: ttlSeconds }); + if (isSidecar) { + await ensureDesktopCache(); + mem.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 }); + debouncedPersist(); return true; - } catch (error) { - console.warn('[Cache] Write failed:', error.message); + } + + const r = await getRedis(); + if (!r) return false; + try { + await r.set(key, value, { ex: ttlSeconds }); + return true; + } catch (err) { + console.warn('[Cache] Write failed:', err.message); return false; } } +export async function mget(...keys) { + if (isSidecar) { + await ensureDesktopCache(); + const now = Date.now(); + return keys.map(k => { + const entry = mem.get(k); + if (!entry || entry.expiresAt <= now) return null; + return entry.value; + }); + } + + const r = await getRedis(); + if (!r) return keys.map(() => null); + try { + return await r.mget(...keys); + } catch (err) { + console.warn('[Cache] mget failed:', err.message); + return keys.map(() => null); + } +} + export function hashString(input) { let hash = 5381; for (let i = 0; i < input.length; i++) { diff --git a/api/acled-conflict.js b/api/acled-conflict.js index 40c2f8830..768f9924a 100644 --- a/api/acled-conflict.js +++ b/api/acled-conflict.js @@ -83,7 +83,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'REDIS-HIT', }, }); @@ -95,7 +95,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'MEMORY-HIT', }, }); @@ -162,7 +162,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'MISS', }, }); @@ -173,7 +173,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', 'X-Cache': 'STALE', }, }); diff --git a/api/acled.js b/api/acled.js index 5c9af0151..71907fdec 100644 --- a/api/acled.js +++ b/api/acled.js @@ -88,7 +88,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'REDIS-HIT', }, }); @@ -100,7 +100,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'MEMORY-HIT', }, }); @@ -171,7 +171,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'MISS', }, }); @@ -182,7 +182,7 @@ export default async function handler(req) { status: 200, headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', 'X-Cache': 'STALE', }, }); diff --git a/api/ais-snapshot.js b/api/ais-snapshot.js index 4b52471ae..18d63dc2e 100644 --- a/api/ais-snapshot.js +++ b/api/ais-snapshot.js @@ -111,7 +111,7 @@ export default async function handler(req) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`, + 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=5`, 'X-Cache': 'REDIS-HIT', ...corsHeaders, }, @@ -125,7 +125,7 @@ export default async function handler(req) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`, + 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=5`, 'X-Cache': 'MEMORY-HIT', ...corsHeaders, }, @@ -135,8 +135,8 @@ export default async function handler(req) { const relayBaseUrl = getRelayBaseUrl(); if (!relayBaseUrl) { recordCacheTelemetry('/api/ais-snapshot', 'NO-RELAY-CONFIG'); - return new Response(JSON.stringify({ error: 'AIS relay not configured' }), { - status: 503, + return new Response(JSON.stringify({ vessels: [], skipped: true, reason: 'AIS relay not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders }, }); } @@ -175,7 +175,7 @@ export default async function handler(req) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`, + 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=5`, 'X-Cache': 'MISS', ...corsHeaders, }, @@ -188,7 +188,7 @@ export default async function handler(req) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`, + 'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=5`, 'X-Cache': 'MEMORY-ERROR-FALLBACK', ...corsHeaders, }, diff --git a/api/arxiv.js b/api/arxiv.js index 13d23e011..9b981ff0d 100644 --- a/api/arxiv.js +++ b/api/arxiv.js @@ -33,7 +33,7 @@ export default async function handler(request) { headers: { 'Content-Type': 'application/xml', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=3600', // 1 hour cache + 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', // 1 hour cache }, }); } catch (error) { diff --git a/api/classify-batch.js b/api/classify-batch.js index eadb6c46e..b41781988 100644 --- a/api/classify-batch.js +++ b/api/classify-batch.js @@ -1,4 +1,4 @@ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, mget, hashString } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -10,34 +10,6 @@ const CACHE_TTL_SECONDS = 86400; const CACHE_VERSION = 'v1'; const MAX_BATCH_SIZE = 20; -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[ClassifyBatch] Redis init failed:', err.message); - redisInitFailed = true; - return null; - } - } - return redis; -} - -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36); -} - const VALID_LEVELS = ['critical', 'high', 'medium', 'low', 'info']; const VALID_CATEGORIES = [ 'conflict', 'protest', 'disaster', 'diplomatic', 'economic', @@ -55,8 +27,8 @@ export default async function handler(request) { const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ fallback: true }), { - status: 503, + return new Response(JSON.stringify({ results: [], fallback: true, skipped: true, reason: 'GROQ_API_KEY not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json' }, }); } @@ -83,33 +55,23 @@ export default async function handler(request) { const results = new Array(batch.length).fill(null); const uncachedIndices = []; - const redisClient = getRedis(); - if (redisClient) { - try { - const cacheKeys = batch.map( - (t) => `classify:${CACHE_VERSION}:${hashString(t.toLowerCase() + ':' + variant)}` - ); - const cached = await redisClient.mget(...cacheKeys); - for (let i = 0; i < cached.length; i++) { - const val = cached[i]; - if (val && typeof val === 'object' && val.level) { - results[i] = { level: val.level, category: val.category, cached: true }; - } else { - uncachedIndices.push(i); - } - } - } catch (e) { - console.warn('[ClassifyBatch] Cache read error:', e.message); - for (let i = 0; i < batch.length; i++) uncachedIndices.push(i); + const cacheKeys = batch.map( + (t) => `classify:${CACHE_VERSION}:${hashString(t.toLowerCase() + ':' + variant)}` + ); + const cached = await mget(...cacheKeys); + for (let i = 0; i < cached.length; i++) { + const val = cached[i]; + if (val && typeof val === 'object' && val.level) { + results[i] = { level: val.level, category: val.category, cached: true }; + } else { + uncachedIndices.push(i); } - } else { - for (let i = 0; i < batch.length; i++) uncachedIndices.push(i); } if (uncachedIndices.length === 0) { return new Response(JSON.stringify({ results }), { status: 200, - headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -190,13 +152,10 @@ Return a JSON array with one object per headline in order: [{"level":"...","cate const idx = uncachedIndices[i]; results[idx] = { level, category, cached: false }; - if (redisClient) { - const cacheKey = `classify:${CACHE_VERSION}:${hashString(batch[idx].toLowerCase() + ':' + variant)}`; - cacheWrites.push( - redisClient.set(cacheKey, { level, category, timestamp: Date.now() }, { ex: CACHE_TTL_SECONDS }) - .catch((e) => console.warn('[ClassifyBatch] Cache write error:', e.message)) - ); - } + const cacheKey = `classify:${CACHE_VERSION}:${hashString(batch[idx].toLowerCase() + ':' + variant)}`; + cacheWrites.push( + setCachedJson(cacheKey, { level, category, timestamp: Date.now() }, CACHE_TTL_SECONDS) + ); } if (cacheWrites.length > 0) { @@ -205,7 +164,7 @@ Return a JSON array with one object per headline in order: [{"level":"...","cate return new Response(JSON.stringify({ results }), { status: 200, - headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } catch (error) { console.error('[ClassifyBatch] Error:', error.message); diff --git a/api/classify-event.js b/api/classify-event.js index ede66d87f..a23ab242d 100644 --- a/api/classify-event.js +++ b/api/classify-event.js @@ -1,4 +1,4 @@ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, hashString } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -9,34 +9,6 @@ const MODEL = 'llama-3.1-8b-instant'; const CACHE_TTL_SECONDS = 86400; const CACHE_VERSION = 'v1'; -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[Classify] Redis init failed:', err.message); - redisInitFailed = true; - return null; - } - } - return redis; -} - -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36); -} - const VALID_LEVELS = ['critical', 'high', 'medium', 'low', 'info']; const VALID_CATEGORIES = [ 'conflict', 'protest', 'disaster', 'diplomatic', 'economic', @@ -54,8 +26,8 @@ export default async function handler(request) { const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ fallback: true }), { - status: 503, + return new Response(JSON.stringify({ fallback: true, skipped: true, reason: 'GROQ_API_KEY not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json' }, }); } @@ -74,25 +46,18 @@ export default async function handler(request) { const cacheKey = `classify:${CACHE_VERSION}:${hashString(title.toLowerCase() + ':' + variant)}`; try { - const redisClient = getRedis(); - if (redisClient) { - try { - const cached = await redisClient.get(cacheKey); - if (cached && typeof cached === 'object' && cached.level) { - return new Response(JSON.stringify({ - level: cached.level, - category: cached.category, - confidence: 0.9, - source: 'llm', - cached: true, - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - } catch (e) { - console.warn('[Classify] Cache read error:', e.message); - } + const cached = await getCachedJson(cacheKey); + if (cached && typeof cached === 'object' && cached.level) { + return new Response(JSON.stringify({ + level: cached.level, + category: cached.category, + confidence: 0.9, + source: 'llm', + cached: true, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); } const isTech = variant === 'tech'; @@ -159,13 +124,7 @@ Return: {"level":"...","category":"..."}`; }); } - if (redisClient) { - try { - await redisClient.set(cacheKey, { level, category, timestamp: Date.now() }, { ex: CACHE_TTL_SECONDS }); - } catch (e) { - console.warn('[Classify] Cache write error:', e.message); - } - } + await setCachedJson(cacheKey, { level, category, timestamp: Date.now() }, CACHE_TTL_SECONDS); return new Response(JSON.stringify({ level, @@ -177,7 +136,7 @@ Return: {"level":"...","category":"..."}`; status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', + 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', }, }); diff --git a/api/climate-anomalies.js b/api/climate-anomalies.js index 4d10654a4..9532cbde4 100644 --- a/api/climate-anomalies.js +++ b/api/climate-anomalies.js @@ -101,7 +101,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/climate-anomalies', 'REDIS-HIT'); return Response.json(cached, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'REDIS-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'REDIS-HIT' }, }); } @@ -109,7 +109,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/climate-anomalies', 'MEMORY-HIT'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MEMORY-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MEMORY-HIT' }, }); } @@ -187,14 +187,14 @@ export default async function handler(req) { return Response.json(result, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MISS' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MISS' }, }); } catch (error) { if (isValidResult(fallbackCache.data)) { recordCacheTelemetry('/api/climate-anomalies', 'STALE'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600', 'X-Cache': 'STALE' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', 'X-Cache': 'STALE' }, }); } diff --git a/api/cloudflare-outages.js b/api/cloudflare-outages.js index 4e5d88a0e..cbcceccb4 100644 --- a/api/cloudflare-outages.js +++ b/api/cloudflare-outages.js @@ -52,7 +52,7 @@ export default async function handler(req) { const data = await response.text(); return new Response(data, { status: response.status, - headers: { 'Content-Type': 'application/json', ...corsHeaders }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=120, s-maxage=120, stale-while-revalidate=60', ...corsHeaders }, }); } catch (error) { // Return empty result on error so client circuit breaker doesn't trigger unnecessarily diff --git a/api/coingecko.js b/api/coingecko.js index 60f302e79..38cd92751 100644 --- a/api/coingecko.js +++ b/api/coingecko.js @@ -9,7 +9,7 @@ const COIN_ID_PATTERN = /^[a-z0-9-]+$/; const CACHE_TTL_SECONDS = 120; // 2 minutes const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000; -const RESPONSE_CACHE_CONTROL = 'public, max-age=120, stale-while-revalidate=60'; +const RESPONSE_CACHE_CONTROL = 'public, max-age=120, s-maxage=120, stale-while-revalidate=60'; const CACHE_VERSION = 'v2'; // In-memory fallback cache for the current instance. diff --git a/api/country-intel.js b/api/country-intel.js index 37b2c5725..e2539c922 100644 --- a/api/country-intel.js +++ b/api/country-intel.js @@ -4,7 +4,7 @@ * Redis cached (2h TTL) for cross-user deduplication */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, hashString } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -15,33 +15,6 @@ const MODEL = 'llama-3.1-8b-instant'; const CACHE_TTL_SECONDS = 7200; // 2 hours const CACHE_VERSION = 'ci-v2'; -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[CountryIntel] Redis init failed:', err.message); - redisInitFailed = true; - } - } - return redis; -} - -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36); -} - export default async function handler(request) { if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { @@ -52,8 +25,8 @@ export default async function handler(request) { const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ error: 'Groq API key not configured', fallback: true }), { - status: 503, + return new Response(JSON.stringify({ intel: null, fallback: true, skipped: true, reason: 'GROQ_API_KEY not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json' }, }); } @@ -72,20 +45,13 @@ export default async function handler(request) { const contextHash = context ? hashString(JSON.stringify(context)).slice(0, 8) : 'no-ctx'; const cacheKey = `${CACHE_VERSION}:${code}:${contextHash}`; - const redisClient = getRedis(); - if (redisClient) { - try { - const cached = await redisClient.get(cacheKey); - if (cached && typeof cached === 'object' && cached.brief) { - console.log('[CountryIntel] Cache hit:', code); - return new Response(JSON.stringify({ ...cached, cached: true }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - } catch (e) { - console.warn('[CountryIntel] Cache read error:', e.message); - } + const cached = await getCachedJson(cacheKey); + if (cached && typeof cached === 'object' && cached.brief) { + console.log('[CountryIntel] Cache hit:', code); + return new Response(JSON.stringify({ ...cached, cached: true }), { + status: 200, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, + }); } // Build data context section @@ -184,19 +150,13 @@ Rules: generatedAt: new Date().toISOString(), }; - // Cache result - if (redisClient && brief) { - try { - await redisClient.set(cacheKey, result, { ex: CACHE_TTL_SECONDS }); - console.log('[CountryIntel] Cached:', code); - } catch (e) { - console.warn('[CountryIntel] Cache write error:', e.message); - } + if (brief) { + await setCachedJson(cacheKey, result, CACHE_TTL_SECONDS); } return new Response(JSON.stringify(result), { status: 200, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } catch (err) { console.error('[CountryIntel] Error:', err); diff --git a/api/earthquakes.js b/api/earthquakes.js index f2ff6178f..47ed82f44 100644 --- a/api/earthquakes.js +++ b/api/earthquakes.js @@ -17,7 +17,7 @@ export default async function handler() { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); } catch (error) { diff --git a/api/eia/[[...path]].js b/api/eia/[[...path]].js index 68cfc4515..17f40751e 100644 --- a/api/eia/[[...path]].js +++ b/api/eia/[[...path]].js @@ -33,10 +33,11 @@ export default async function handler(req) { if (!apiKey) { return Response.json({ - error: 'EIA API not configured', configured: false, + skipped: true, + reason: 'EIA_API_KEY not configured', }, { - status: 503, + status: 200, headers: { 'Access-Control-Allow-Origin': corsOrigin }, }); } @@ -113,7 +114,7 @@ export default async function handler(req) { return Response.json(results, { headers: { 'Access-Control-Allow-Origin': corsOrigin, - 'Cache-Control': 'public, max-age=1800', // 30 min cache + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', // 30 min cache }, }); } catch (error) { diff --git a/api/etf-flows.js b/api/etf-flows.js index a9ddf3862..f8b21ccb2 100644 --- a/api/etf-flows.js +++ b/api/etf-flows.js @@ -157,7 +157,7 @@ export default async function handler(req) { cacheTimestamp = now; return new Response(JSON.stringify(fallback), { status: 200, - headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=60' }, + headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=30, s-maxage=60, stale-while-revalidate=30' }, }); } } diff --git a/api/faa-status.js b/api/faa-status.js index 0c27d5f63..7e62c8fde 100644 --- a/api/faa-status.js +++ b/api/faa-status.js @@ -11,6 +11,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/xml', 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', }, }); } catch (error) { diff --git a/api/finnhub.js b/api/finnhub.js index e9d973802..b822f0af5 100644 --- a/api/finnhub.js +++ b/api/finnhub.js @@ -76,9 +76,9 @@ export default async function handler(req) { const apiKey = process.env.FINNHUB_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ error: 'Finnhub API key not configured' }), { - status: 503, - headers: { 'Content-Type': 'application/json', ...corsHeaders }, + return new Response(JSON.stringify({ quotes: [], skipped: true, reason: 'FINNHUB_API_KEY not configured' }), { + status: 200, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', ...corsHeaders }, }); } @@ -102,7 +102,7 @@ export default async function handler(req) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=30', + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', ...corsHeaders, }, }); diff --git a/api/firms-fires.js b/api/firms-fires.js index 5fdcd622d..6da2214b0 100644 --- a/api/firms-fires.js +++ b/api/firms-fires.js @@ -71,7 +71,7 @@ function parseCSV(csv) { export default async function handler(request) { if (!FIRMS_API_KEY) { - return json({ error: 'FIRMS_API_KEY not configured' }, 503); + return json({ regions: {}, totalCount: 0, skipped: true, reason: 'NASA_FIRMS_API_KEY not configured', source: SOURCE, days: 0, timestamp: new Date().toISOString() }); } try { @@ -132,7 +132,7 @@ function json(data, status = 200) { status, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=600', // 10 min cache + 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', // 10 min cache }, }); } diff --git a/api/fred-data.js b/api/fred-data.js index 5483df84c..37f29bd12 100644 --- a/api/fred-data.js +++ b/api/fred-data.js @@ -68,7 +68,7 @@ export default async function handler(req) { status: response.status, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=3600', + 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', ...corsHeaders, }, }); diff --git a/api/fwdstart.js b/api/fwdstart.js index e8f1d6d5d..1a97af711 100644 --- a/api/fwdstart.js +++ b/api/fwdstart.js @@ -85,7 +85,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/xml; charset=utf-8', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=1800', + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); } catch (error) { diff --git a/api/gdelt-doc.js b/api/gdelt-doc.js index 56c23f3b0..e49f79313 100644 --- a/api/gdelt-doc.js +++ b/api/gdelt-doc.js @@ -51,7 +51,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); } catch (error) { diff --git a/api/gdelt-geo.js b/api/gdelt-geo.js index c81bec91a..e69c17214 100644 --- a/api/gdelt-geo.js +++ b/api/gdelt-geo.js @@ -89,7 +89,7 @@ export default async function handler(req) { headers: { 'Content-Type': format === 'csv' ? 'text/csv' : 'application/json', 'Access-Control-Allow-Origin': corsOrigin, - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); } catch (error) { diff --git a/api/github-trending.js b/api/github-trending.js index 50b1f7bf1..c4d8500b2 100644 --- a/api/github-trending.js +++ b/api/github-trending.js @@ -51,7 +51,7 @@ export default async function handler(request) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=1800', // 30 min cache + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', // 30 min cache }, }); } @@ -63,7 +63,7 @@ export default async function handler(request) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=1800', // 30 min cache + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', // 30 min cache }, }); } catch (error) { diff --git a/api/groq-summarize.js b/api/groq-summarize.js index d30c3f933..c28b600c0 100644 --- a/api/groq-summarize.js +++ b/api/groq-summarize.js @@ -5,7 +5,7 @@ * Server-side Redis cache for cross-user deduplication */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, hashString } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -15,49 +15,15 @@ const GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions'; const MODEL = 'llama-3.1-8b-instant'; // 14.4K RPD vs 1K for 70b const CACHE_TTL_SECONDS = 86400; // 24 hours -// Initialize Redis (lazy - only if env vars present) -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[Groq] Redis init failed:', err.message); - redisInitFailed = true; - return null; - } - } - return redis; -} - -// Cache version - increment to bust old caches after breaking changes const CACHE_VERSION = 'v3'; -// Generate cache key from headlines, geoContext, and variant function getCacheKey(headlines, mode, geoContext = '', variant = 'full') { const sorted = headlines.slice(0, 8).sort().join('|'); const geoHash = geoContext ? ':g' + hashString(geoContext).slice(0, 6) : ''; const hash = hashString(`${mode}:${sorted}`); - // Include variant and version to prevent cross-site cache collisions return `summary:${CACHE_VERSION}:${variant}:${hash}${geoHash}`; } -// Simple hash function for cache keys -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36); -} - // Deduplicate similar headlines (same story from different sources) function deduplicateHeadlines(headlines) { const seen = new Set(); @@ -104,8 +70,8 @@ export default async function handler(request) { const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ error: 'Groq API key not configured', fallback: true }), { - status: 503, + return new Response(JSON.stringify({ summary: null, fallback: true, skipped: true, reason: 'GROQ_API_KEY not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json' }, }); } @@ -120,28 +86,20 @@ export default async function handler(request) { }); } - // Check Redis cache first - const redisClient = getRedis(); + // Check cache first const cacheKey = getCacheKey(headlines, mode, geoContext, variant); - - if (redisClient) { - try { - const cached = await redisClient.get(cacheKey); - if (cached && typeof cached === 'object' && cached.summary) { - console.log('[Groq] Cache hit:', cacheKey); - return new Response(JSON.stringify({ - summary: cached.summary, - model: cached.model || MODEL, - provider: 'cache', - cached: true, - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - } catch (cacheError) { - console.warn('[Groq] Cache read error:', cacheError.message); - } + const cached = await getCachedJson(cacheKey); + if (cached && typeof cached === 'object' && cached.summary) { + console.log('[Groq] Cache hit:', cacheKey); + return new Response(JSON.stringify({ + summary: cached.summary, + model: cached.model || MODEL, + provider: 'cache', + cached: true, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); } // Deduplicate similar headlines (same story from multiple sources) @@ -261,19 +219,12 @@ Rules: }); } - // Store in Redis cache - if (redisClient) { - try { - await redisClient.set(cacheKey, { - summary, - model: MODEL, - timestamp: Date.now(), - }, { ex: CACHE_TTL_SECONDS }); - console.log('[Groq] Cached:', cacheKey); - } catch (cacheError) { - console.warn('[Groq] Cache write error:', cacheError.message); - } - } + // Store in cache + await setCachedJson(cacheKey, { + summary, + model: MODEL, + timestamp: Date.now(), + }, CACHE_TTL_SECONDS); return new Response(JSON.stringify({ summary, @@ -285,7 +236,7 @@ Rules: status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=1800', + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); diff --git a/api/hackernews.js b/api/hackernews.js index f9fe53d5d..01cc0deae 100644 --- a/api/hackernews.js +++ b/api/hackernews.js @@ -71,7 +71,7 @@ export default async function handler(request) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=300', // 5 min cache + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', // 5 min cache }, }); } catch (error) { diff --git a/api/hapi.js b/api/hapi.js index 115ef0785..2923c196d 100644 --- a/api/hapi.js +++ b/api/hapi.js @@ -128,7 +128,7 @@ export default async function handler(req) { status: 200, headers: { 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', 'X-Cache': 'STALE', }, }); diff --git a/api/macro-signals.js b/api/macro-signals.js index 870c3d5ea..24a257f40 100644 --- a/api/macro-signals.js +++ b/api/macro-signals.js @@ -278,7 +278,7 @@ export default async function handler(req) { cacheTimestamp = now; return new Response(JSON.stringify(fallback), { status: 200, - headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=60' }, + headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=30, s-maxage=60, stale-while-revalidate=30' }, }); } } diff --git a/api/nga-warnings.js b/api/nga-warnings.js index 584949651..8eeeb3d0b 100644 --- a/api/nga-warnings.js +++ b/api/nga-warnings.js @@ -8,7 +8,7 @@ export default async function handler(req) { const data = await response.text(); return new Response(data, { status: response.status, - headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60' }, }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { diff --git a/api/og-story.js b/api/og-story.js index a7801cd09..a5a0b1bbf 100644 --- a/api/og-story.js +++ b/api/og-story.js @@ -215,7 +215,7 @@ export default function handler(req, res) { `; res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600'); + res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600'); res.status(200).send(svg); } diff --git a/api/openrouter-summarize.js b/api/openrouter-summarize.js index 63d1db625..de9ee6957 100644 --- a/api/openrouter-summarize.js +++ b/api/openrouter-summarize.js @@ -6,7 +6,7 @@ * Server-side Redis cache for cross-user deduplication */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, hashString } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -16,39 +16,15 @@ const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; const MODEL = 'meta-llama/llama-3.3-70b-instruct:free'; const CACHE_TTL_SECONDS = 86400; // 24 hours -// Initialize Redis (lazy - only if env vars present) -let redis = null; -function getRedis() { - if (redis) return redis; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - redis = new Redis({ url, token }); - } - return redis; -} - -// Cache version - increment to bust old caches after breaking changes const CACHE_VERSION = 'v3'; -// Generate cache key from headlines, geoContext, and variant (same as groq endpoint) function getCacheKey(headlines, mode, geoContext = '', variant = 'full') { const sorted = headlines.slice(0, 8).sort().join('|'); const geoHash = geoContext ? ':g' + hashString(geoContext).slice(0, 6) : ''; const hash = hashString(`${mode}:${sorted}`); - // Include variant and version to prevent cross-site cache collisions return `summary:${CACHE_VERSION}:${variant}:${hash}${geoHash}`; } -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash).toString(36); -} - // Deduplicate similar headlines (same story from different sources) function deduplicateHeadlines(headlines) { const seen = new Set(); @@ -95,8 +71,8 @@ export default async function handler(request) { const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { - return new Response(JSON.stringify({ error: 'OpenRouter API key not configured', fallback: true }), { - status: 503, + return new Response(JSON.stringify({ summary: null, fallback: true, skipped: true, reason: 'OPENROUTER_API_KEY not configured' }), { + status: 200, headers: { 'Content-Type': 'application/json' }, }); } @@ -111,28 +87,20 @@ export default async function handler(request) { }); } - // Check Redis cache first (shared with Groq endpoint) - const redisClient = getRedis(); + // Check cache first (shared with Groq endpoint) const cacheKey = getCacheKey(headlines, mode, geoContext, variant); - - if (redisClient) { - try { - const cached = await redisClient.get(cacheKey); - if (cached && typeof cached === 'object' && cached.summary) { - console.log('[OpenRouter] Cache hit:', cacheKey); - return new Response(JSON.stringify({ - summary: cached.summary, - model: cached.model || MODEL, - provider: 'cache', - cached: true, - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - } catch (cacheError) { - console.warn('[OpenRouter] Cache read error:', cacheError.message); - } + const cached = await getCachedJson(cacheKey); + if (cached && typeof cached === 'object' && cached.summary) { + console.log('[OpenRouter] Cache hit:', cacheKey); + return new Response(JSON.stringify({ + summary: cached.summary, + model: cached.model || MODEL, + provider: 'cache', + cached: true, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); } // Deduplicate similar headlines (same story from different sources) @@ -254,19 +222,12 @@ Rules: }); } - // Store in Redis cache (shared with Groq endpoint) - if (redisClient) { - try { - await redisClient.set(cacheKey, { - summary, - model: MODEL, - timestamp: Date.now(), - }, { ex: CACHE_TTL_SECONDS }); - console.log('[OpenRouter] Cached:', cacheKey); - } catch (cacheError) { - console.warn('[OpenRouter] Cache write error:', cacheError.message); - } - } + // Store in cache (shared with Groq endpoint) + await setCachedJson(cacheKey, { + summary, + model: MODEL, + timestamp: Date.now(), + }, CACHE_TTL_SECONDS); return new Response(JSON.stringify({ summary, @@ -278,7 +239,7 @@ Rules: status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=1800', + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); diff --git a/api/opensky.js b/api/opensky.js index 9877c060e..d2294f219 100644 --- a/api/opensky.js +++ b/api/opensky.js @@ -51,7 +51,7 @@ export default async function handler(req) { status: response.status, headers: { 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=30', + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', }, }); } catch (error) { diff --git a/api/pizzint/dashboard-data.js b/api/pizzint/dashboard-data.js index d54aada4e..ad9ab689e 100644 --- a/api/pizzint/dashboard-data.js +++ b/api/pizzint/dashboard-data.js @@ -19,7 +19,7 @@ export default async function handler() { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', }, }); } catch (error) { diff --git a/api/pizzint/gdelt/batch.js b/api/pizzint/gdelt/batch.js index b2e270504..d54b2cfca 100644 --- a/api/pizzint/gdelt/batch.js +++ b/api/pizzint/gdelt/batch.js @@ -29,7 +29,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); } catch (error) { diff --git a/api/polymarket.js b/api/polymarket.js index cfb8b5758..b3c6a6ae4 100644 --- a/api/polymarket.js +++ b/api/polymarket.js @@ -1,5 +1,7 @@ export const config = { runtime: 'edge' }; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; + const ALLOWED_ORDER = ['volume', 'liquidity', 'startDate', 'endDate', 'spread']; const MAX_LIMIT = 100; const MIN_LIMIT = 1; @@ -24,6 +26,32 @@ function sanitizeTagSlug(val) { return val.replace(/[^a-z0-9-]/gi, '').slice(0, 100) || null; } +async function tryFetch(url, timeoutMs = 8000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { + headers: { 'Accept': 'application/json' }, + signal: controller.signal, + }); + clearTimeout(timer); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.text(); + } catch (err) { + clearTimeout(timer); + throw err; + } +} + +function buildUrl(base, endpoint, params) { + if (endpoint === 'events') { + return `${base}/events?${params}`; + } + return `${base}/markets?${params}`; +} + export default async function handler(req) { const url = new URL(req.url); const endpoint = url.searchParams.get('endpoint') || 'markets'; @@ -33,40 +61,42 @@ export default async function handler(req) { const ascending = validateBoolean(url.searchParams.get('ascending'), 'false'); const limit = validateLimit(url.searchParams.get('limit')); + const params = new URLSearchParams({ + closed, + order, + ascending, + limit: String(limit), + }); + + if (endpoint === 'events') { + const tag = sanitizeTagSlug(url.searchParams.get('tag')); + if (tag) params.set('tag_slug', tag); + } + + // Gamma API is behind Cloudflare which blocks server-side TLS connections + // (JA3 fingerprint detection). Only browser-originated requests succeed. + // We still try in case Cloudflare policy changes, but gracefully return empty on failure. try { - let polyUrl; - - if (endpoint === 'events') { - const tag = sanitizeTagSlug(url.searchParams.get('tag')); - const params = new URLSearchParams({ - closed: closed, - order: order, - ascending: ascending, - limit: String(limit), - }); - if (tag) params.set('tag_slug', tag); - polyUrl = `https://gamma-api.polymarket.com/events?${params}`; - } else { - polyUrl = `https://gamma-api.polymarket.com/markets?closed=${closed}&order=${order}&ascending=${ascending}&limit=${limit}`; - } - - const response = await fetch(polyUrl, { - headers: { 'Accept': 'application/json' }, - }); - - const data = await response.text(); + const data = await tryFetch(buildUrl(GAMMA_BASE, endpoint, params)); return new Response(data, { - status: response.status, + status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=120', + 'Cache-Control': 'public, max-age=120, s-maxage=120, stale-while-revalidate=60', + 'X-Polymarket-Source': 'gamma', }, }); - } catch (error) { - return new Response(JSON.stringify({ error: 'Failed to fetch data' }), { - status: 500, - headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, + } catch (err) { + // Expected: Cloudflare blocks non-browser TLS connections + return new Response(JSON.stringify([]), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'X-Polymarket-Error': err.message, + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', + }, }); } } diff --git a/api/risk-scores.js b/api/risk-scores.js index cb79361fc..85b2b6517 100644 --- a/api/risk-scores.js +++ b/api/risk-scores.js @@ -4,7 +4,7 @@ * Uses Upstash Redis for cross-user caching (10-minute TTL) */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -61,18 +61,6 @@ const COUNTRY_KEYWORDS = { VE: ['venezuela', 'caracas', 'maduro'], }; -// Initialize Redis (lazy) -let redis = null; -function getRedis() { - if (redis) return redis; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - redis = new Redis({ url, token }); - } - return redis; -} - function normalizeCountryName(text) { const lower = text.toLowerCase(); for (const [code, keywords] of Object.entries(COUNTRY_KEYWORDS)) { @@ -241,34 +229,38 @@ export default async function handler(request) { } if (!process.env.ACLED_ACCESS_TOKEN) { - return new Response(JSON.stringify({ error: 'ACLED_ACCESS_TOKEN not configured' }), { - status: 503, - headers: { 'Content-Type': 'application/json' }, + const baselineScores = computeCIIScores([]); + const baselineStrategic = computeStrategicRisk(baselineScores); + return new Response(JSON.stringify({ + cii: baselineScores, + strategicRisk: baselineStrategic, + protestCount: 0, + computedAt: new Date().toISOString(), + baseline: true, + error: 'ACLED token not configured - showing baseline risk assessments', + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', + }, }); } - const redisClient = getRedis(); - // Check cache first - if (redisClient) { - try { - const cached = await redisClient.get(CACHE_KEY); - if (cached && typeof cached === 'object') { - console.log('[RiskScores] Cache hit'); - return new Response(JSON.stringify({ - ...cached, - cached: true, - }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=300', - }, - }); - } - } catch (cacheError) { - console.warn('[RiskScores] Cache read error:', cacheError.message); - } + const cached = await getCachedJson(CACHE_KEY); + if (cached && typeof cached === 'object') { + console.log('[RiskScores] Cache hit'); + return new Response(JSON.stringify({ + ...cached, + cached: true, + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', + }, + }); } try { @@ -289,18 +281,11 @@ export default async function handler(request) { computedAt: new Date().toISOString(), }; - // Cache in Redis (both regular and stale backup) - if (redisClient) { - try { - await Promise.all([ - redisClient.set(CACHE_KEY, result, { ex: CACHE_TTL_SECONDS }), - redisClient.set(STALE_CACHE_KEY, result, { ex: STALE_CACHE_TTL_SECONDS }), - ]); - console.log('[RiskScores] Cached scores'); - } catch (cacheError) { - console.warn('[RiskScores] Cache write error:', cacheError.message); - } - } + // Cache (both regular and stale backup) + await Promise.all([ + setCachedJson(CACHE_KEY, result, CACHE_TTL_SECONDS), + setCachedJson(STALE_CACHE_KEY, result, STALE_CACHE_TTL_SECONDS), + ]); return new Response(JSON.stringify({ ...result, @@ -309,7 +294,7 @@ export default async function handler(request) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); @@ -317,27 +302,21 @@ export default async function handler(request) { console.error('[RiskScores] Error:', error); // Try to return stale cached data - if (redisClient) { - try { - const stale = await redisClient.get(STALE_CACHE_KEY); - if (stale && typeof stale === 'object') { - console.log('[RiskScores] Returning stale cache due to error'); - return new Response(JSON.stringify({ - ...stale, - cached: true, - stale: true, - error: 'Using cached data - ACLED temporarily unavailable', - }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=60', - }, - }); - } - } catch (cacheError) { - console.warn('[RiskScores] Stale cache read error:', cacheError.message); - } + const stale = await getCachedJson(STALE_CACHE_KEY); + if (stale && typeof stale === 'object') { + console.log('[RiskScores] Returning stale cache due to error'); + return new Response(JSON.stringify({ + ...stale, + cached: true, + stale: true, + error: 'Using cached data - ACLED temporarily unavailable', + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', + }, + }); } // Final fallback: return baseline scores without unrest data @@ -356,7 +335,7 @@ export default async function handler(request) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', }, }); } diff --git a/api/rss-proxy.js b/api/rss-proxy.js index baae066da..2f2c396b2 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -223,7 +223,7 @@ export default async function handler(req) { status: response.status, headers: { 'Content-Type': 'application/xml', - 'Cache-Control': 'public, max-age=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', ...corsHeaders, }, }); diff --git a/api/service-status.js b/api/service-status.js index 5cbcefe83..82f56785b 100644 --- a/api/service-status.js +++ b/api/service-status.js @@ -285,7 +285,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=60', // 1 min cache + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', // 1 min cache }, }); } diff --git a/api/stablecoin-markets.js b/api/stablecoin-markets.js index ea16e47ff..7395da3d5 100644 --- a/api/stablecoin-markets.js +++ b/api/stablecoin-markets.js @@ -60,7 +60,7 @@ export default async function handler(req) { if (res.status === 429) { if (cachedResponse) { return new Response(JSON.stringify(cachedResponse), { - headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=60' }, + headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=30, s-maxage=60, stale-while-revalidate=30' }, }); } return new Response(JSON.stringify({ error: 'Rate limited', timestamp: new Date().toISOString() }), { @@ -124,7 +124,7 @@ export default async function handler(req) { cacheTimestamp = now; return new Response(JSON.stringify(fallback), { status: 200, - headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=60' }, + headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=30, s-maxage=60, stale-while-revalidate=30' }, }); } } diff --git a/api/stock-index.js b/api/stock-index.js index 37012c8e4..5877106f7 100644 --- a/api/stock-index.js +++ b/api/stock-index.js @@ -4,7 +4,7 @@ * Redis cached (1h TTL) */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -61,29 +61,11 @@ const COUNTRY_INDEX = { HU: { symbol: '^BUX', name: 'BUX' }, }; -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[StockIndex] Redis init failed:', err.message); - redisInitFailed = true; - } - } - return redis; -} - export default async function handler(request) { if (request.method !== 'GET') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -93,7 +75,7 @@ export default async function handler(request) { if (!code) { return new Response(JSON.stringify({ error: 'code parameter required' }), { status: 400, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -101,25 +83,17 @@ export default async function handler(request) { if (!index) { return new Response(JSON.stringify({ error: 'No stock index for country', code, available: false }), { status: 200, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } const cacheKey = `${CACHE_VERSION}:${code}`; - const redisClient = getRedis(); - - if (redisClient) { - try { - const cached = await redisClient.get(cacheKey); - if (cached && typeof cached === 'object' && cached.indexName) { - return new Response(JSON.stringify({ ...cached, cached: true }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - } catch (e) { - console.warn('[StockIndex] Cache read error:', e.message); - } + const cached = await getCachedJson(cacheKey); + if (cached && typeof cached === 'object' && cached.indexName) { + return new Response(JSON.stringify({ ...cached, cached: true }), { + status: 200, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, + }); } try { @@ -137,7 +111,7 @@ export default async function handler(request) { console.error('[StockIndex] Yahoo error:', res.status, index.symbol); return new Response(JSON.stringify({ error: 'Upstream error', available: false }), { status: 502, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -146,7 +120,7 @@ export default async function handler(request) { if (!result) { return new Response(JSON.stringify({ error: 'No data', available: false }), { status: 200, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -154,7 +128,7 @@ export default async function handler(request) { if (!allCloses || allCloses.length < 2) { return new Response(JSON.stringify({ error: 'Insufficient data', available: false }), { status: 200, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } @@ -176,23 +150,17 @@ export default async function handler(request) { fetchedAt: new Date().toISOString(), }; - if (redisClient) { - try { - await redisClient.set(cacheKey, payload, { ex: CACHE_TTL_SECONDS }); - } catch (e) { - console.warn('[StockIndex] Cache write error:', e.message); - } - } + await setCachedJson(cacheKey, payload, CACHE_TTL_SECONDS); return new Response(JSON.stringify(payload), { status: 200, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } catch (err) { console.error('[StockIndex] Error:', err); return new Response(JSON.stringify({ error: 'Internal error', available: false }), { status: 500, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } } diff --git a/api/story.js b/api/story.js index c7d23eccd..809480c6a 100644 --- a/api/story.js +++ b/api/story.js @@ -75,7 +75,7 @@ export default function handler(req, res) { `; res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=300'); + res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=300, stale-while-revalidate=60'); res.status(200).send(html); } diff --git a/api/tech-events.js b/api/tech-events.js index 0eab60e92..9c766db09 100644 --- a/api/tech-events.js +++ b/api/tech-events.js @@ -713,7 +713,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, s-maxage=1800', // Cache for 30 minutes + 'Cache-Control': 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=300', }, }); } catch (error) { diff --git a/api/temporal-baseline.js b/api/temporal-baseline.js index c56a7b1e6..8f49177a4 100644 --- a/api/temporal-baseline.js +++ b/api/temporal-baseline.js @@ -7,7 +7,7 @@ * POST { updates: [{ type, region, count }] } — batch update baselines */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson, mget } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -21,27 +21,6 @@ const Z_THRESHOLD_HIGH = 3.0; const VALID_TYPES = ['military_flights', 'vessels', 'protests', 'news', 'ais_gaps', 'satellite_fires']; -// Lazy Redis init (same pattern as groq-summarize.js) -let redis = null; -let redisInitFailed = false; -function getRedis() { - if (redis) return redis; - if (redisInitFailed) return null; - - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[TemporalBaseline] Redis init failed:', err.message); - redisInitFailed = true; - return null; - } - } - return redis; -} - function makeKey(type, region, weekday, month) { return `baseline:${type}:${region}:${weekday}:${month}`; } @@ -54,19 +33,11 @@ function getSeverity(zScore) { } export default async function handler(request) { - const r = getRedis(); - if (!r) { - return new Response(JSON.stringify({ error: 'Redis not configured' }), { - status: 503, - headers: { 'Content-Type': 'application/json' }, - }); - } - try { if (request.method === 'GET') { - return await handleGet(r, request); + return await handleGet(request); } else if (request.method === 'POST') { - return await handlePost(r, request); + return await handlePost(request); } return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, @@ -81,7 +52,7 @@ export default async function handler(request) { } } -async function handleGet(r, request) { +async function handleGet(request) { const { searchParams } = new URL(request.url); const type = searchParams.get('type'); const region = searchParams.get('region') || 'global'; @@ -96,7 +67,7 @@ async function handleGet(r, request) { const month = now.getUTCMonth() + 1; const key = makeKey(type, region, weekday, month); - const baseline = await r.get(key); + const baseline = await getCachedJson(key); if (!baseline || baseline.sampleCount < MIN_SAMPLES) { return json({ @@ -130,7 +101,7 @@ async function handleGet(r, request) { }); } -async function handlePost(r, request) { +async function handlePost(request) { const body = await request.json(); const updates = body?.updates; @@ -138,19 +109,15 @@ async function handlePost(r, request) { return json({ error: 'Body must have updates array' }, 400); } - // Cap batch size const batch = updates.slice(0, 20); const now = new Date(); const weekday = now.getUTCDay(); const month = now.getUTCMonth() + 1; - // Read all existing baselines const keys = batch.map(u => makeKey(u.type, u.region || 'global', weekday, month)); - const existing = await r.mget(...keys); + const existing = await mget(...keys); - // Compute Welford updates and pipeline writes - const pipeline = r.pipeline(); - let updated = 0; + const writes = []; for (let i = 0; i < batch.length; i++) { const { type, region = 'global', count } = batch[i]; @@ -158,28 +125,25 @@ async function handlePost(r, request) { const prev = existing[i] || { mean: 0, m2: 0, sampleCount: 0 }; - // Welford's online algorithm const n = prev.sampleCount + 1; const delta = count - prev.mean; const newMean = prev.mean + delta / n; const delta2 = count - newMean; const newM2 = prev.m2 + delta * delta2; - pipeline.set(keys[i], { + writes.push(setCachedJson(keys[i], { mean: newMean, m2: newM2, sampleCount: n, lastUpdated: now.toISOString(), - }, { ex: BASELINE_TTL }); - - updated++; + }, BASELINE_TTL)); } - if (updated > 0) { - await pipeline.exec(); + if (writes.length > 0) { + await Promise.all(writes); } - return json({ updated }); + return json({ updated: writes.length }); } function json(data, status = 200) { diff --git a/api/theater-posture.js b/api/theater-posture.js index 27fd4c87c..2af0fd41c 100644 --- a/api/theater-posture.js +++ b/api/theater-posture.js @@ -4,7 +4,7 @@ * TTL: 5 minutes (matches OpenSky refresh rate) */ -import { Redis } from '@upstash/redis'; +import { getCachedJson, setCachedJson } from './_upstash-cache.js'; export const config = { runtime: 'edge', @@ -219,29 +219,16 @@ function isMilitaryCallsign(callsign) { return false; } -// Initialize Redis -let redis = null; -function getRedis() { - if (redis) return redis; - const url = process.env.UPSTASH_REDIS_REST_URL; - const token = process.env.UPSTASH_REDIS_REST_TOKEN; - if (url && token) { - try { - redis = new Redis({ url, token }); - } catch (err) { - console.warn('[TheaterPosture] Redis init failed:', err.message); - return null; - } - } - return redis; -} - -// Fetch military flights from OpenSky via Railway relay (avoids rate limits) +// Fetch military flights from OpenSky async function fetchMilitaryFlights() { - // Use Railway relay to avoid OpenSky rate limits - // VITE_* vars aren't available server-side, so check WS_RELAY_URL or use known production URL - const relayUrl = process.env.WS_RELAY_URL || 'https://worldmonitor-production-ws.up.railway.app'; - const baseUrl = relayUrl + '/opensky'; + const isSidecar = (process.env.LOCAL_API_MODE || '').includes('sidecar'); + // Desktop sidecar: fetch directly from OpenSky (single user, no rate limit concern) + // Cloud: use Railway relay to avoid OpenSky rate limits across many users + const baseUrl = isSidecar + ? 'https://opensky-network.org/api/states/all' + : (process.env.WS_RELAY_URL ? process.env.WS_RELAY_URL + '/opensky' : null); + + if (!baseUrl) return []; // Fetch global data with 20s timeout (Edge has 25s limit) const controller = new AbortController(); @@ -518,25 +505,18 @@ export default async function handler(req) { try { // Try to get from cache first - const redisClient = getRedis(); - if (redisClient) { - try { - const cached = await redisClient.get(CACHE_KEY); - if (cached) { - console.log('[TheaterPosture] Cache hit'); - return Response.json({ - ...cached, - cached: true, - }, { - headers: { - ...corsHeaders, - 'Cache-Control': 'public, max-age=60', - }, - }); - } - } catch (err) { - console.warn('[TheaterPosture] Cache read error:', err.message); - } + const cached = await getCachedJson(CACHE_KEY); + if (cached) { + console.log('[TheaterPosture] Cache hit'); + return Response.json({ + ...cached, + cached: true, + }, { + headers: { + ...corsHeaders, + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', + }, + }); } // Fetch and calculate - try OpenSky first, then Wingbits fallback @@ -571,72 +551,52 @@ export default async function handler(req) { }; // Cache the result (regular, stale, and long-term backup) - if (redisClient) { - try { - await Promise.all([ - redisClient.set(CACHE_KEY, result, { ex: CACHE_TTL_SECONDS }), - redisClient.set(STALE_CACHE_KEY, result, { ex: STALE_CACHE_TTL_SECONDS }), - redisClient.set(BACKUP_CACHE_KEY, result, { ex: BACKUP_CACHE_TTL_SECONDS }), - ]); - console.log('[TheaterPosture] Cached result (5min/24h/7d)'); - } catch (err) { - console.warn('[TheaterPosture] Cache write error:', err.message); - } - } + await Promise.all([ + setCachedJson(CACHE_KEY, result, CACHE_TTL_SECONDS), + setCachedJson(STALE_CACHE_KEY, result, STALE_CACHE_TTL_SECONDS), + setCachedJson(BACKUP_CACHE_KEY, result, BACKUP_CACHE_TTL_SECONDS), + ]); return Response.json(result, { headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', }, }); } catch (error) { console.warn('[TheaterPosture] Error:', error.message); // Try to return cached data when API fails (stale first, then backup) - const staleRedisClient = getRedis(); - if (staleRedisClient) { - // Try stale cache (24h TTL) - try { - const stale = await staleRedisClient.get(STALE_CACHE_KEY); - if (stale) { - console.log('[TheaterPosture] Returning stale cached data (24h) due to API error'); - return Response.json({ - ...stale, - cached: true, - stale: true, - error: 'Using cached data - live feed temporarily unavailable', - }, { - headers: { - ...corsHeaders, - 'Cache-Control': 'public, max-age=30', - }, - }); - } - } catch (cacheErr) { - console.warn('[TheaterPosture] Stale cache read error:', cacheErr.message); - } + const stale = await getCachedJson(STALE_CACHE_KEY); + if (stale) { + console.log('[TheaterPosture] Returning stale cached data (24h) due to API error'); + return Response.json({ + ...stale, + cached: true, + stale: true, + error: 'Using cached data - live feed temporarily unavailable', + }, { + headers: { + ...corsHeaders, + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', + }, + }); + } - // Try backup cache (7d TTL) as last resort - try { - const backup = await staleRedisClient.get(BACKUP_CACHE_KEY); - if (backup) { - console.log('[TheaterPosture] Returning backup cached data (7d) due to API error'); - return Response.json({ - ...backup, - cached: true, - stale: true, - error: 'Using backup data - live feed temporarily unavailable', - }, { - headers: { - ...corsHeaders, - 'Cache-Control': 'public, max-age=30', - }, - }); - } - } catch (cacheErr) { - console.warn('[TheaterPosture] Backup cache read error:', cacheErr.message); - } + const backup = await getCachedJson(BACKUP_CACHE_KEY); + if (backup) { + console.log('[TheaterPosture] Returning backup cached data (7d) due to API error'); + return Response.json({ + ...backup, + cached: true, + stale: true, + error: 'Using backup data - live feed temporarily unavailable', + }, { + headers: { + ...corsHeaders, + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', + }, + }); } // No cached data available - return error diff --git a/api/ucdp-events.js b/api/ucdp-events.js index 3defbb94d..52f3aa4eb 100644 --- a/api/ucdp-events.js +++ b/api/ucdp-events.js @@ -136,7 +136,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/ucdp-events', 'REDIS-HIT'); return Response.json(cached, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'REDIS-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'REDIS-HIT' }, }); } @@ -144,7 +144,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/ucdp-events', 'MEMORY-HIT'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MEMORY-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MEMORY-HIT' }, }); } @@ -218,14 +218,14 @@ export default async function handler(req) { return Response.json(result, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MISS' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MISS' }, }); } catch (error) { if (isValidResult(fallbackCache.data)) { recordCacheTelemetry('/api/ucdp-events', 'STALE'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600', 'X-Cache': 'STALE' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', 'X-Cache': 'STALE' }, }); } diff --git a/api/ucdp.js b/api/ucdp.js index 5c02d8819..c8015d3c7 100644 --- a/api/ucdp.js +++ b/api/ucdp.js @@ -130,7 +130,7 @@ export default async function handler(req) { status: 200, headers: { 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=600', + 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', 'X-Cache': 'STALE', }, }); diff --git a/api/unhcr-population.js b/api/unhcr-population.js index 9dea673d7..75b2649fc 100644 --- a/api/unhcr-population.js +++ b/api/unhcr-population.js @@ -104,7 +104,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/unhcr-population', 'REDIS-HIT'); return Response.json(cached, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'REDIS-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'REDIS-HIT' }, }); } @@ -112,7 +112,7 @@ export default async function handler(req) { recordCacheTelemetry('/api/unhcr-population', 'MEMORY-HIT'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MEMORY-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MEMORY-HIT' }, }); } @@ -251,14 +251,14 @@ export default async function handler(req) { return Response.json(result, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600', 'X-Cache': 'MISS' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MISS' }, }); } catch (error) { if (isValidResult(fallbackCache.data)) { recordCacheTelemetry('/api/unhcr-population', 'STALE'); return Response.json(fallbackCache.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600', 'X-Cache': 'STALE' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', 'X-Cache': 'STALE' }, }); } diff --git a/api/wingbits/[[...path]].js b/api/wingbits/[[...path]].js index 0808d3f9d..bcb0ce486 100644 --- a/api/wingbits/[[...path]].js +++ b/api/wingbits/[[...path]].js @@ -70,7 +70,7 @@ export default async function handler(req) { return Response.json(data, { headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=86400', // 24h - aircraft details rarely change + 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600', // 24h - aircraft details rarely change }, }); } catch (error) { @@ -194,7 +194,7 @@ export default async function handler(req) { return Response.json(data, { headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=30', // 30 seconds - live data + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', // 30 seconds - live data }, }); } catch (error) { @@ -259,7 +259,7 @@ export default async function handler(req) { return Response.json(data, { headers: { ...corsHeaders, - 'Cache-Control': 'public, max-age=30', + 'Cache-Control': 'public, max-age=30, s-maxage=30, stale-while-revalidate=15', }, }); } catch (error) { diff --git a/api/wingbits/details/[icao24].js b/api/wingbits/details/[icao24].js index 2e8190503..ec58764a0 100644 --- a/api/wingbits/details/[icao24].js +++ b/api/wingbits/details/[icao24].js @@ -49,7 +49,7 @@ export default async function handler(req, { params }) { const data = await response.json(); return Response.json(data, { - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600' }, }); } catch (error) { return Response.json({ error: error.message, icao24 }, { status: 500, headers: corsHeaders }); diff --git a/api/wingbits/details/batch.js b/api/wingbits/details/batch.js index 5cae9fb22..ce9ba6fb0 100644 --- a/api/wingbits/details/batch.js +++ b/api/wingbits/details/batch.js @@ -67,7 +67,7 @@ export default async function handler(req) { results, fetched: Object.keys(results).length, requested: limitedList.length, - }, { headers: corsHeaders }); + }, { headers: { 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', ...corsHeaders } }); } catch (error) { return Response.json({ error: error.message }, { status: 500, headers: corsHeaders }); } diff --git a/api/worldbank.js b/api/worldbank.js index ec4546db4..bd5587ee2 100644 --- a/api/worldbank.js +++ b/api/worldbank.js @@ -1,4 +1,8 @@ -// Node.js serverless function (Edge gets 403 from World Bank) +// World Bank API proxy (Web API handler for Edge + sidecar compatibility) + +export const config = { + runtime: 'edge', +}; const TECH_INDICATORS = { 'IT.NET.USER.ZS': 'Internet Users (% of population)', @@ -20,61 +24,56 @@ const TECH_INDICATORS = { }; const TECH_COUNTRIES = [ - // Major tech economies 'USA', 'CHN', 'JPN', 'DEU', 'KOR', 'GBR', 'IND', 'ISR', 'SGP', 'TWN', 'FRA', 'CAN', 'SWE', 'NLD', 'CHE', 'FIN', 'IRL', 'AUS', 'BRA', 'IDN', - // Middle East & emerging tech hubs 'ARE', 'SAU', 'QAT', 'BHR', 'EGY', 'TUR', - // Additional Asia 'MYS', 'THA', 'VNM', 'PHL', - // Europe 'ESP', 'ITA', 'POL', 'CZE', 'DNK', 'NOR', 'AUT', 'BEL', 'PRT', 'EST', - // Americas 'MEX', 'ARG', 'CHL', 'COL', - // Africa 'ZAF', 'NGA', 'KEN', ]; -export default async function handler(req, res) { - // CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); +const CORS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', +}; - if (req.method === 'OPTIONS') { - return res.status(200).end(); +function json(data, status = 200, extra = {}) { + return new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json', ...CORS, ...extra }, + }); +} + +export default async function handler(request) { + if (request.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: CORS }); } - const { indicator, country, countries, years = '5', action } = req.query; + const url = new URL(request.url); + const indicator = url.searchParams.get('indicator'); + const country = url.searchParams.get('country'); + const countries = url.searchParams.get('countries'); + const years = url.searchParams.get('years') || '5'; + const action = url.searchParams.get('action'); - // Return available indicators if (action === 'indicators') { - res.setHeader('Cache-Control', 'public, max-age=86400'); - return res.json({ - indicators: TECH_INDICATORS, - defaultCountries: TECH_COUNTRIES, - }); + return json({ indicators: TECH_INDICATORS, defaultCountries: TECH_COUNTRIES }, 200, { 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600' }); } - // Validate indicator if (!indicator) { - return res.status(400).json({ - error: 'Missing indicator parameter', - availableIndicators: Object.keys(TECH_INDICATORS), - }); + return json({ error: 'Missing indicator parameter', availableIndicators: Object.keys(TECH_INDICATORS) }, 400); } try { - // Build country list let countryList = country || countries || TECH_COUNTRIES.join(';'); if (countries) { countryList = countries.split(',').join(';'); } - // Calculate date range const currentYear = new Date().getFullYear(); const startYear = currentYear - parseInt(years); - // World Bank API v2 const wbUrl = `https://api.worldbank.org/v2/country/${countryList}/indicator/${indicator}?format=json&date=${startYear}:${currentYear}&per_page=1000`; const response = await fetch(wbUrl, { @@ -90,30 +89,23 @@ export default async function handler(req, res) { const data = await response.json(); - // World Bank returns [metadata, data] array if (!data || !Array.isArray(data) || data.length < 2 || !data[1]) { - res.setHeader('Cache-Control', 'public, max-age=3600'); - return res.json({ + return json({ indicator, indicatorName: TECH_INDICATORS[indicator] || indicator, metadata: { page: 1, pages: 1, total: 0 }, byCountry: {}, latestByCountry: {}, timeSeries: [], - }); + }, 200, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }); } const [metadata, records] = data; - // Transform data for easier frontend consumption const transformed = { indicator, indicatorName: TECH_INDICATORS[indicator] || (records[0]?.indicator?.value || indicator), - metadata: { - page: metadata.page, - pages: metadata.pages, - total: metadata.total, - }, + metadata: { page: metadata.page, pages: metadata.pages, total: metadata.total }, byCountry: {}, latestByCountry: {}, timeSeries: [], @@ -128,46 +120,25 @@ export default async function handler(req, res) { if (!countryCode || value === null) continue; if (!transformed.byCountry[countryCode]) { - transformed.byCountry[countryCode] = { - code: countryCode, - name: countryName, - values: [], - }; + transformed.byCountry[countryCode] = { code: countryCode, name: countryName, values: [] }; } transformed.byCountry[countryCode].values.push({ year, value }); - if (!transformed.latestByCountry[countryCode] || - year > transformed.latestByCountry[countryCode].year) { - transformed.latestByCountry[countryCode] = { - code: countryCode, - name: countryName, - year, - value, - }; + if (!transformed.latestByCountry[countryCode] || year > transformed.latestByCountry[countryCode].year) { + transformed.latestByCountry[countryCode] = { code: countryCode, name: countryName, year, value }; } - transformed.timeSeries.push({ - countryCode, - countryName, - year, - value, - }); + transformed.timeSeries.push({ countryCode, countryName, year, value }); } - // Sort each country's values by year for (const c of Object.values(transformed.byCountry)) { c.values.sort((a, b) => a.year - b.year); } - // Sort time series by year descending transformed.timeSeries.sort((a, b) => b.year - a.year || a.countryCode.localeCompare(b.countryCode)); - res.setHeader('Cache-Control', 'public, max-age=3600'); - return res.json(transformed); + return json(transformed, 200, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }); } catch (error) { - return res.status(500).json({ - error: error.message, - indicator, - }); + return json({ error: error.message, indicator }, 500); } } diff --git a/api/worldpop-exposure.js b/api/worldpop-exposure.js index 3909f2a54..200b2878a 100644 --- a/api/worldpop-exposure.js +++ b/api/worldpop-exposure.js @@ -62,7 +62,7 @@ async function handleCountries(corsHeaders, now) { recordCacheTelemetry('/api/worldpop-exposure?countries', 'REDIS-HIT'); return Response.json(cached, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400', 'X-Cache': 'REDIS-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600', 'X-Cache': 'REDIS-HIT' }, }); } @@ -70,7 +70,7 @@ async function handleCountries(corsHeaders, now) { recordCacheTelemetry('/api/worldpop-exposure?countries', 'MEMORY-HIT'); return Response.json(countriesFallback.data, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400', 'X-Cache': 'MEMORY-HIT' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600', 'X-Cache': 'MEMORY-HIT' }, }); } @@ -88,7 +88,7 @@ async function handleCountries(corsHeaders, now) { return Response.json(result, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400', 'X-Cache': 'MISS' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600', 'X-Cache': 'MISS' }, }); } @@ -125,7 +125,7 @@ function handleExposure(corsHeaders, lat, lon, radius) { densityPerKm2: Math.round(density), }, { status: 200, - headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600' }, + headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' }, }); } diff --git a/api/yahoo-finance.js b/api/yahoo-finance.js index 85f398b02..ed47cd1ac 100644 --- a/api/yahoo-finance.js +++ b/api/yahoo-finance.js @@ -36,7 +36,7 @@ export default async function handler(req) { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=60', + 'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=30', }, }); } catch (error) { diff --git a/api/youtube/live.js b/api/youtube/live.js index 18353d1ae..7218617e5 100644 --- a/api/youtube/live.js +++ b/api/youtube/live.js @@ -46,7 +46,7 @@ export default async function handler(request) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, s-maxage=300', // Cache for 5 minutes + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', // Cache for 5 minutes }, }); } @@ -56,7 +56,7 @@ export default async function handler(request) { status: 200, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, s-maxage=300', + 'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60', }, }); } catch (error) { diff --git a/index.html b/index.html index a2fba980f..c60954a27 100644 --- a/index.html +++ b/index.html @@ -89,7 +89,7 @@ - + diff --git a/package-lock.json b/package-lock.json index 5c6e9f2b4..969509d4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "world-monitor", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "world-monitor", - "version": "2.1.4", + "version": "2.2.0", "dependencies": { "@deck.gl/aggregation-layers": "^9.2.6", "@deck.gl/core": "^9.2.6", @@ -31,6 +31,7 @@ "@types/topojson-specification": "^1.0.5", "typescript": "^5.7.2", "vite": "^6.0.7", + "vite-plugin-pwa": "^1.2.0", "ws": "^8.19.0" } }, @@ -75,6 +76,24 @@ "license": "MIT", "peer": true }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, "node_modules/@arcgis/core": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.34.8.tgz", @@ -124,6 +143,1580 @@ "tslib": "^2.8.1" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bufbuild/protobuf": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", @@ -1027,6 +2620,77 @@ "license": "MIT", "peer": true }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", @@ -1757,6 +3421,77 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -2107,6 +3842,19 @@ "win32" ] }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, "node_modules/@turf/boolean-clockwise": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-5.1.5.tgz", @@ -2530,6 +4278,13 @@ "license": "MIT", "peer": true }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sortablejs": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", @@ -2578,8 +4333,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@upstash/redis": { "version": "1.36.1", @@ -2892,6 +4646,36 @@ "gl-matrix": "^3.4.3" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2901,6 +4685,88 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -2915,6 +4781,71 @@ } } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/bare-events": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", @@ -3026,6 +4957,16 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3051,6 +4992,19 @@ "node": ">= 6" } }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/brotli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", @@ -3060,6 +5014,40 @@ "base64-js": "^1.1.2" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buf-compare": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", @@ -3093,12 +5081,18 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -3117,7 +5111,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -3131,7 +5124,6 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -3143,6 +5135,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/cartocolor": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/cartocolor/-/cartocolor-5.0.2.tgz", @@ -3247,6 +5260,16 @@ "node": ">= 10" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/composed-offset-position": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", @@ -3257,6 +5280,13 @@ "@floating-ui/utils": "^0.2.5" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-assert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", @@ -3270,12 +5300,41 @@ "node": ">=0.10.0" } }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -3292,6 +5351,16 @@ "license": "MIT", "peer": true }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cssfilter": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", @@ -3837,6 +5906,78 @@ "node": ">=12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deck.gl": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/deck.gl/-/deck.gl-9.2.6.tgz", @@ -3934,12 +6075,21 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3957,7 +6107,6 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -4006,7 +6155,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -4022,6 +6170,29 @@ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "license": "ISC" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -4031,12 +6202,80 @@ "once": "^1.4.0" } }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -4046,7 +6285,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -4056,7 +6294,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -4064,6 +6301,40 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-toolkit": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", @@ -4117,6 +6388,16 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esri-loader": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/esri-loader/-/esri-loader-3.7.0.tgz", @@ -4124,6 +6405,23 @@ "deprecated": "Use @arcgis/core instead.", "license": "Apache-2.0" }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -4142,12 +6440,43 @@ "node": ">=6" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -4190,6 +6519,46 @@ "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/flatbuffers": { "version": "25.9.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", @@ -4213,12 +6582,61 @@ "tabbable": "^6.4.0" } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4239,7 +6657,27 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4249,11 +6687,30 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/geojson-vt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", @@ -4265,7 +6722,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -4285,12 +6741,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", - "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -4311,6 +6773,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4323,12 +6803,41 @@ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", "license": "MIT" }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, "engines": { "node": ">= 0.4" }, @@ -4336,6 +6845,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", @@ -4353,12 +6881,24 @@ "yarn": ">=1.3.0" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -4366,12 +6906,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4384,7 +6939,6 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", - "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4400,7 +6954,6 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", - "peer": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4420,6 +6973,13 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4480,6 +7040,21 @@ "@interactjs/types": "1.10.27" } }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -4506,24 +7081,141 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -4541,12 +7233,107 @@ "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==", "license": "MIT" }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -4560,12 +7347,202 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jpeg-exif": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", @@ -4574,6 +7551,13 @@ "license": "MIT", "peer": true }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -4583,12 +7567,75 @@ "node": ">= 10.16.0" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-pretty-compact": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4613,6 +7660,16 @@ "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==", "license": "MIT" }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -4656,6 +7713,27 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, "node_modules/long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", @@ -4665,6 +7743,16 @@ "node": ">=0.6" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -4688,6 +7776,16 @@ "integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==", "license": "BSD-2-Clause" }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/maplibre-gl": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.16.0.tgz", @@ -4786,7 +7884,6 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -4823,6 +7920,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -4832,6 +7945,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mjolnir.js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.0.0.tgz", @@ -4865,6 +7988,13 @@ "node": "*" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/murmurhash-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", @@ -4914,6 +8044,26 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-is": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", @@ -4936,11 +8086,31 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5038,12 +8208,81 @@ "integrity": "sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==", "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/pbf": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", @@ -5186,6 +8425,16 @@ "license": "ISC", "peer": true }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5299,6 +8548,19 @@ "node": ">=6" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5351,6 +8613,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/quadbin": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/quadbin/-/quadbin-0.4.2.tgz", @@ -5369,6 +8641,16 @@ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -5422,12 +8704,54 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5443,6 +8767,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -5509,12 +8902,81 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5557,12 +9019,21 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5580,7 +9051,6 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5591,6 +9061,21 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -5661,6 +9146,118 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -5715,6 +9312,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smob": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/snappyjs": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", @@ -5728,6 +9335,20 @@ "license": "MIT", "peer": true }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5738,12 +9359,55 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", @@ -5764,6 +9428,118 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -5811,6 +9587,19 @@ "kdbush": "^4.0.2" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/svg-arc-to-cubic-bezier": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", @@ -5850,6 +9639,74 @@ "streamx": "^2.15.0" } }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -5932,6 +9789,16 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5964,6 +9831,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5978,6 +9923,25 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/uncrypto": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", @@ -5990,6 +9954,40 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicode-properties": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", @@ -6001,6 +9999,16 @@ "unicode-trie": "^2.0.0" } }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicode-trie": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", @@ -6019,6 +10027,71 @@ "license": "MIT", "peer": true }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6100,12 +10173,493 @@ } } }, + "node_modules/vite-plugin-pwa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/wgsl_reflect": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz", "integrity": "sha512-BQWBIsOn411M+ffBxmA6QRLvAOVbuz3Uk4NusxnqC1H7aeQcVLhdA3k2k/EFFFtqVjhz3z7JOOZF1a9hj2tv4Q==", "license": "MIT" }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-build": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.79.2", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6171,6 +10725,13 @@ "license": "MIT", "peer": true }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/youtubei.js": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-16.0.1.tgz", diff --git a/package.json b/package.json index 6f6973a6f..a6bf6d091 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "world-monitor", "private": true, - "version": "2.1.4", + "version": "2.2.0", "type": "module", "scripts": { "dev": "vite", @@ -44,6 +44,7 @@ "@types/topojson-specification": "^1.0.5", "typescript": "^5.7.2", "vite": "^6.0.7", + "vite-plugin-pwa": "^1.2.0", "ws": "^8.19.0" }, "dependencies": { diff --git a/public/favico/site.webmanifest b/public/favico/site.webmanifest deleted file mode 100644 index f3afa262c..000000000 --- a/public/favico/site.webmanifest +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "World Monitor - AI Intelligence Dashboard", - "short_name": "WorldMonitor", - "description": "AI-powered real-time global intelligence dashboard", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#0c162d", - "background_color": "#0c162d", - "display": "standalone" -} \ No newline at end of file diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 000000000..e9040dd0c --- /dev/null +++ b/public/offline.html @@ -0,0 +1,26 @@ + + + + + + WorldMonitor - Offline + + + +
+

You're Offline

+

WorldMonitor requires an internet connection for real-time intelligence data.

+ +
+ + diff --git a/scripts/ais-relay.cjs b/scripts/ais-relay.cjs index 27589a4c0..65b9146c0 100644 --- a/scripts/ais-relay.cjs +++ b/scripts/ais-relay.cjs @@ -729,8 +729,7 @@ const server = http.createServer(async (req, res) => { } if (req.url === '/health' || req.url === '/') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ + sendCompressed(req, res, 200, { 'Content-Type': 'application/json' }, JSON.stringify({ status: 'ok', clients: clients.size, messages: messageCount, @@ -801,12 +800,11 @@ const server = http.createServer(async (req, res) => { // Serve from cache if fresh (5 min TTL) const rssCached = rssResponseCache.get(feedUrl); if (rssCached && Date.now() - rssCached.timestamp < RSS_CACHE_TTL_MS) { - res.writeHead(200, { + return sendCompressed(req, res, 200, { 'Content-Type': rssCached.contentType || 'application/xml', 'Cache-Control': 'public, max-age=300', 'X-Cache': 'HIT', - }); - return res.end(rssCached.data); + }, rssCached.data); } console.log('[Relay] RSS request (MISS):', feedUrl); @@ -862,12 +860,11 @@ const server = http.createServer(async (req, res) => { if (response.statusCode >= 200 && response.statusCode < 300) { rssResponseCache.set(feedUrl, { data, contentType: 'application/xml', timestamp: Date.now() }); } - res.writeHead(response.statusCode, { + sendCompressed(req, res, response.statusCode, { 'Content-Type': 'application/xml', 'Cache-Control': 'public, max-age=300', 'X-Cache': 'MISS', - }); - res.end(data); + }, data); }); stream.on('error', (err) => { console.error('[Relay] Decompression error:', err.message); @@ -881,8 +878,7 @@ const server = http.createServer(async (req, res) => { if (rssCached) { if (!responseHandled && !res.headersSent) { responseHandled = true; - res.writeHead(200, { 'Content-Type': 'application/xml', 'X-Cache': 'STALE' }); - res.end(rssCached.data); + sendCompressed(req, res, 200, { 'Content-Type': 'application/xml', 'X-Cache': 'STALE' }, rssCached.data); } return; } @@ -893,8 +889,7 @@ const server = http.createServer(async (req, res) => { request.destroy(); if (rssCached && !responseHandled && !res.headersSent) { responseHandled = true; - res.writeHead(200, { 'Content-Type': 'application/xml', 'X-Cache': 'STALE' }); - return res.end(rssCached.data); + return sendCompressed(req, res, 200, { 'Content-Type': 'application/xml', 'X-Cache': 'STALE' }, rssCached.data); } sendError(504, 'Request timeout'); }); diff --git a/settings.html b/settings.html index 7c56feabf..005278ebd 100644 --- a/settings.html +++ b/settings.html @@ -7,18 +7,44 @@
-
-
-

Settings

-

Desktop runtime configuration and local API keys.

-
-
- - -
-
+
+ + +

-
+
+
+
+
+
+
+ + +
+
+
+

Diagnostics

+
+ + +
+
+
+

API Traffic

+
+ + + +
+
+
+
+
+
+
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 25c47482d..4686946f2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "world-monitor" -version = "0.1.0" +version = "2.2.0" description = "World Monitor desktop application" authors = ["World Monitor"] edition = "2021" @@ -13,6 +13,7 @@ tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" keyring = "3" +reqwest = { version = "0.12", default-features = false, features = ["native-tls", "json"] } [features] default = ["custom-protocol"] diff --git a/src-tauri/sidecar/local-api-server.mjs b/src-tauri/sidecar/local-api-server.mjs index 013cb9718..f78f6fb93 100644 --- a/src-tauri/sidecar/local-api-server.mjs +++ b/src-tauri/sidecar/local-api-server.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node import { createServer } from 'node:http'; -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { readdir } from 'node:fs/promises'; +import { gzipSync } from 'node:zlib'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -158,6 +159,44 @@ const failedImports = new Set(); const fallbackCounts = new Map(); const cloudPreferred = new Set(); +const TRAFFIC_LOG_MAX = 200; +const trafficLog = []; +let verboseMode = false; +let _verboseStatePath = null; + +function loadVerboseState(resourceDir) { + _verboseStatePath = path.join(resourceDir, 'verbose-mode.json'); + try { + const data = JSON.parse(readFileSync(_verboseStatePath, 'utf-8')); + verboseMode = !!data.verboseMode; + } catch { /* file missing or invalid — keep default false */ } +} + +function saveVerboseState() { + if (!_verboseStatePath) return; + try { writeFileSync(_verboseStatePath, JSON.stringify({ verboseMode })); } catch { /* ignore */ } +} + +function recordTraffic(entry) { + trafficLog.push(entry); + if (trafficLog.length > TRAFFIC_LOG_MAX) trafficLog.shift(); + if (verboseMode) { + const ts = entry.timestamp.split('T')[1].replace('Z', ''); + console.log(`[traffic] ${ts} ${entry.method} ${entry.path} → ${entry.status} ${entry.durationMs}ms`); + } +} + +function logOnce(logger, route, message) { + const key = `${route}:${message}`; + const count = (fallbackCounts.get(key) || 0) + 1; + fallbackCounts.set(key, count); + if (count === 1) { + logger.warn(`[local-api] ${route} → ${message}`); + } else if (count === 5 || count % 100 === 0) { + logger.warn(`[local-api] ${route} → ${message} (x${count})`); + } +} + async function importHandler(modulePath) { if (failedImports.has(modulePath)) { throw new Error(`cached-failure:${path.basename(modulePath)}`); @@ -189,6 +228,7 @@ function resolveConfig(options = {}) { path.join(resourceDir, '_up_', 'api'), ].find((candidate) => existsSync(candidate)) ?? path.join(resourceDir, 'api'); const mode = String(options.mode ?? process.env.LOCAL_API_MODE ?? 'desktop-sidecar'); + const cloudFallback = String(options.cloudFallback ?? process.env.LOCAL_API_CLOUD_FALLBACK ?? '') === 'true'; const logger = options.logger ?? console; return { @@ -197,6 +237,7 @@ function resolveConfig(options = {}) { resourceDir, apiDir, mode, + cloudFallback, logger, }; } @@ -241,7 +282,18 @@ async function tryCloudFallback(requestUrl, req, context, reason) { } } +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', +}; + async function dispatch(requestUrl, req, routes, context) { + if (req.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + if (requestUrl.pathname === '/api/service-status') { return handleLocalServiceStatus(context); } @@ -252,82 +304,171 @@ async function dispatch(requestUrl, req, routes, context) { port: context.port, apiDir: context.apiDir, remoteBase: context.remoteBase, + cloudFallback: context.cloudFallback, routes: routes.length, }); } + if (requestUrl.pathname === '/api/local-traffic-log') { + if (req.method === 'DELETE') { + trafficLog.length = 0; + return json({ cleared: true }); + } + return json({ entries: [...trafficLog], verboseMode, maxEntries: TRAFFIC_LOG_MAX }); + } + if (requestUrl.pathname === '/api/local-debug-toggle') { + if (req.method === 'POST') { + verboseMode = !verboseMode; + saveVerboseState(); + context.logger.log(`[local-api] verbose logging ${verboseMode ? 'ON' : 'OFF'}`); + } + return json({ verboseMode }); + } + if (requestUrl.pathname === '/api/local-env-update') { + if (req.method === 'POST') { + const body = await readBody(req); + if (body) { + try { + const { key, value } = JSON.parse(body.toString()); + if (typeof key === 'string' && key.length > 0 && key.length < 100) { + if (value == null || value === '') { + delete process.env[key]; + context.logger.log(`[local-api] env unset: ${key}`); + } else { + process.env[key] = String(value); + context.logger.log(`[local-api] env set: ${key}`); + } + // Clear cached handler modules so they pick up new env on next call + moduleCache.clear(); + failedImports.clear(); + cloudPreferred.clear(); + return json({ ok: true, key }); + } + } catch { /* bad JSON */ } + } + return json({ error: 'expected { key, value }' }, 400); + } + return json({ error: 'POST required' }, 405); + } - if (cloudPreferred.has(requestUrl.pathname)) { + if (context.cloudFallback && cloudPreferred.has(requestUrl.pathname)) { const cloudResponse = await tryCloudFallback(requestUrl, req, context); if (cloudResponse) return cloudResponse; } const modulePath = pickModule(requestUrl.pathname, routes); if (!modulePath || !existsSync(modulePath)) { - const cloudResponse = await tryCloudFallback(requestUrl, req, context, 'handler missing'); - if (cloudResponse) return cloudResponse; - return json({ error: 'Local handler missing and cloud fallback unavailable' }, 502); + if (context.cloudFallback) { + const cloudResponse = await tryCloudFallback(requestUrl, req, context, 'handler missing'); + if (cloudResponse) return cloudResponse; + } + logOnce(context.logger, requestUrl.pathname, 'no local handler'); + return json({ error: 'No local handler for this endpoint', endpoint: requestUrl.pathname }, 404); } try { const mod = await importHandler(modulePath); if (typeof mod.default !== 'function') { - const cloudResponse = await tryCloudFallback(requestUrl, req, context, `invalid handler module ${path.basename(modulePath)}`); - if (cloudResponse) return cloudResponse; - return json({ error: `Invalid handler module: ${path.basename(modulePath)}` }, 500); + logOnce(context.logger, requestUrl.pathname, 'invalid handler module'); + if (context.cloudFallback) { + const cloudResponse = await tryCloudFallback(requestUrl, req, context, `invalid handler module`); + if (cloudResponse) return cloudResponse; + } + return json({ error: 'Invalid handler module', endpoint: requestUrl.pathname }, 500); } const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await readBody(req); const request = new Request(requestUrl.toString(), { method: req.method, - // Local handler execution does not need browser-origin metadata. headers: toHeaders(req.headers, { stripOrigin: true }), body, }); const response = await mod.default(request); if (!(response instanceof Response)) { - const cloudResponse = await tryCloudFallback(requestUrl, req, context, 'handler returned non-Response'); - if (cloudResponse) { cloudPreferred.add(requestUrl.pathname); return cloudResponse; } - return json({ error: `Handler returned invalid response for ${requestUrl.pathname}` }, 500); + logOnce(context.logger, requestUrl.pathname, 'handler returned non-Response'); + if (context.cloudFallback) { + const cloudResponse = await tryCloudFallback(requestUrl, req, context, 'handler returned non-Response'); + if (cloudResponse) return cloudResponse; + } + return json({ error: 'Handler returned invalid response', endpoint: requestUrl.pathname }, 500); } - // Local handlers can return 4xx/5xx when desktop keys are missing. - // Prefer cloud parity response when available. - if (!response.ok) { + if (!response.ok && context.cloudFallback) { const cloudResponse = await tryCloudFallback(requestUrl, req, context, `local status ${response.status}`); if (cloudResponse) { cloudPreferred.add(requestUrl.pathname); return cloudResponse; } } return response; } catch (error) { - const cloudResponse = await tryCloudFallback(requestUrl, req, context, error); - if (cloudResponse) { cloudPreferred.add(requestUrl.pathname); return cloudResponse; } - return json({ error: 'Local handler failed and cloud fallback unavailable' }, 502); + const reason = error.code === 'ERR_MODULE_NOT_FOUND' ? 'missing dependency' : error.message; + context.logger.error(`[local-api] ${requestUrl.pathname} → ${reason}`); + if (context.cloudFallback) { + const cloudResponse = await tryCloudFallback(requestUrl, req, context, error); + if (cloudResponse) { cloudPreferred.add(requestUrl.pathname); return cloudResponse; } + } + return json({ error: 'Local handler error', reason, endpoint: requestUrl.pathname }, 502); } } export async function createLocalApiServer(options = {}) { const context = resolveConfig(options); + loadVerboseState(context.resourceDir); const routes = await buildRouteTable(context.apiDir); const server = createServer(async (req, res) => { const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${context.port}`); if (!requestUrl.pathname.startsWith('/api/')) { - res.writeHead(404, { 'content-type': 'application/json' }); + res.writeHead(404, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); res.end(JSON.stringify({ error: 'Not found' })); return; } + const start = Date.now(); + const skipRecord = requestUrl.pathname === '/api/local-traffic-log' || requestUrl.pathname === '/api/local-debug-toggle' || requestUrl.pathname === '/api/local-env-update'; + try { const response = await dispatch(requestUrl, req, routes, context); - const body = Buffer.from(await response.arrayBuffer()); + const durationMs = Date.now() - start; + let body = Buffer.from(await response.arrayBuffer()); const headers = Object.fromEntries(response.headers.entries()); + headers['access-control-allow-origin'] = '*'; + + if (!skipRecord) { + recordTraffic({ + timestamp: new Date().toISOString(), + method: req.method, + path: requestUrl.pathname + (requestUrl.search || ''), + status: response.status, + durationMs, + }); + } + + const acceptEncoding = req.headers['accept-encoding'] || ''; + if (acceptEncoding.includes('gzip') && body.length > 1024) { + body = gzipSync(body); + headers['content-encoding'] = 'gzip'; + headers['vary'] = 'Accept-Encoding'; + } + res.writeHead(response.status, headers); res.end(body); } catch (error) { + const durationMs = Date.now() - start; context.logger.error('[local-api] fatal', error); - res.writeHead(500, { 'content-type': 'application/json' }); + + if (!skipRecord) { + recordTraffic({ + timestamp: new Date().toISOString(), + method: req.method, + path: requestUrl.pathname + (requestUrl.search || ''), + status: 500, + durationMs, + error: error.message, + }); + } + + res.writeHead(500, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); res.end(JSON.stringify({ error: 'Internal server error' })); } }); @@ -354,7 +495,7 @@ export async function createLocalApiServer(options = {}) { const address = server.address(); const boundPort = typeof address === 'object' && address?.port ? address.port : context.port; - context.logger.log(`[local-api] listening on http://127.0.0.1:${boundPort} (apiDir=${context.apiDir}, routes=${routes.length})`); + context.logger.log(`[local-api] listening on http://127.0.0.1:${boundPort} (apiDir=${context.apiDir}, routes=${routes.length}, cloudFallback=${context.cloudFallback})`); return { port: boundPort }; }, async close() { diff --git a/src-tauri/sidecar/local-api-server.test.mjs b/src-tauri/sidecar/local-api-server.test.mjs index 581b9428e..949f950c9 100644 --- a/src-tauri/sidecar/local-api-server.test.mjs +++ b/src-tauri/sidecar/local-api-server.test.mjs @@ -99,7 +99,7 @@ async function setupResourceDirWithUpApi(files) { }; } -test('falls back to cloud when local handler returns a 500', async () => { +test('returns local error directly when cloudFallback is off (default)', async () => { const remote = await setupRemoteServer(); const localApi = await setupApiDir({ 'fred-data.js': ` @@ -120,6 +120,41 @@ test('falls back to cloud when local handler returns a 500', async () => { }); const { port } = await app.start(); + try { + const response = await fetch(`http://127.0.0.1:${port}/api/fred-data`); + assert.equal(response.status, 500); + const body = await response.json(); + assert.equal(body.source, 'local-error'); + assert.equal(remote.hits.length, 0); + } finally { + await app.close(); + await localApi.cleanup(); + await remote.close(); + } +}); + +test('falls back to cloud when cloudFallback is enabled and local handler returns 500', async () => { + const remote = await setupRemoteServer(); + const localApi = await setupApiDir({ + 'fred-data.js': ` + export default async function handler() { + return new Response(JSON.stringify({ source: 'local-error' }), { + status: 500, + headers: { 'content-type': 'application/json' } + }); + } + `, + }); + + const app = await createLocalApiServer({ + port: 0, + apiDir: localApi.apiDir, + remoteBase: remote.remoteBase, + cloudFallback: 'true', + logger: { log() {}, warn() {}, error() {} }, + }); + const { port } = await app.start(); + try { const response = await fetch(`http://127.0.0.1:${port}/api/fred-data`); assert.equal(response.status, 200); @@ -167,7 +202,7 @@ test('uses local handler response when local handler succeeds', async () => { } }); -test('falls back to cloud when local route does not exist', async () => { +test('returns 404 when local route does not exist and cloudFallback is off', async () => { const remote = await setupRemoteServer(); const localApi = await setupApiDir({}); @@ -181,10 +216,10 @@ test('falls back to cloud when local route does not exist', async () => { try { const response = await fetch(`http://127.0.0.1:${port}/api/not-found`); - assert.equal(response.status, 200); + assert.equal(response.status, 404); const body = await response.json(); - assert.equal(body.source, 'remote'); - assert.equal(remote.hits.includes('/api/not-found'), true); + assert.equal(body.error, 'No local handler for this endpoint'); + assert.equal(remote.hits.length, 0); } finally { await app.close(); await localApi.cleanup(); @@ -233,7 +268,7 @@ test('strips browser origin headers before invoking local handlers', async () => } }); -test('strips browser origin headers when proxying to cloud fallback', async () => { +test('strips browser origin headers when proxying to cloud fallback (cloudFallback enabled)', async () => { const remote = await setupRemoteServer(); const localApi = await setupApiDir({}); @@ -241,6 +276,7 @@ test('strips browser origin headers when proxying to cloud fallback', async () = port: 0, apiDir: localApi.apiDir, remoteBase: remote.remoteBase, + cloudFallback: 'true', logger: { log() {}, warn() {}, error() {} }, }); const { port } = await app.start(); @@ -261,6 +297,32 @@ test('strips browser origin headers when proxying to cloud fallback', async () = } }); +test('responds to OPTIONS preflight with CORS headers', async () => { + const localApi = await setupApiDir({ + 'data.js': ` + export default async function handler() { + return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }); + } + `, + }); + + const app = await createLocalApiServer({ + port: 0, + apiDir: localApi.apiDir, + logger: { log() {}, warn() {}, error() {} }, + }); + const { port } = await app.start(); + + try { + const response = await fetch(`http://127.0.0.1:${port}/api/data`, { method: 'OPTIONS' }); + assert.equal(response.status, 204); + assert.equal(response.headers.get('access-control-allow-methods'), 'GET, POST, PUT, DELETE, OPTIONS'); + } finally { + await app.close(); + await localApi.cleanup(); + } +}); + test('resolves packaged tauri resource layout under _up_/api', async () => { const remote = await setupRemoteServer(); const localResource = await setupResourceDirWithUpApi({ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index efb8ba2b2..2e8f2d536 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,7 +10,7 @@ use std::env; use keyring::Entry; use serde_json::{Map, Value}; -use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu}; use tauri::{AppHandle, Manager, RunEvent, WebviewUrl, WebviewWindowBuilder}; const LOCAL_API_PORT: &str = "46123"; @@ -18,8 +18,7 @@ const KEYRING_SERVICE: &str = "world-monitor"; const LOCAL_API_LOG_FILE: &str = "local-api.log"; const DESKTOP_LOG_FILE: &str = "desktop.log"; const MENU_FILE_SETTINGS_ID: &str = "file.settings"; -const MENU_DEBUG_OPEN_LOGS_ID: &str = "debug.open_logs"; -const MENU_DEBUG_OPEN_SIDECAR_LOG_ID: &str = "debug.open_sidecar_log"; +const MENU_HELP_GITHUB_ID: &str = "help.github"; const SUPPORTED_SECRET_KEYS: [&str; 15] = [ "GROQ_API_KEY", "OPENROUTER_API_KEY", @@ -169,32 +168,44 @@ fn append_desktop_log(app: &AppHandle, level: &str, message: &str) { let _ = writeln!(file, "[{timestamp}][{level}] {message}"); } -fn open_path_in_shell(path: &Path) -> Result<(), String> { +fn open_in_shell(arg: &str) -> Result<(), String> { #[cfg(target_os = "macos")] let mut command = { let mut cmd = Command::new("open"); - cmd.arg(path); + cmd.arg(arg); cmd }; #[cfg(target_os = "windows")] let mut command = { let mut cmd = Command::new("explorer"); - cmd.arg(path); + cmd.arg(arg); cmd }; #[cfg(all(unix, not(target_os = "macos")))] let mut command = { let mut cmd = Command::new("xdg-open"); - cmd.arg(path); + cmd.arg(arg); cmd }; command .spawn() .map(|_| ()) - .map_err(|e| format!("Failed to open {}: {e}", path.display())) + .map_err(|e| format!("Failed to open {}: {e}", arg)) +} + +fn open_path_in_shell(path: &Path) -> Result<(), String> { + open_in_shell(&path.to_string_lossy()) +} + +#[tauri::command] +fn open_url(url: String) -> Result<(), String> { + if !url.starts_with("https://") && !url.starts_with("http://") { + return Err("Only http/https URLs are allowed".to_string()); + } + open_in_shell(&url) } fn open_logs_folder_impl(app: &AppHandle) -> Result { @@ -228,6 +239,41 @@ fn open_settings_window_command(app: AppHandle) -> Result<(), String> { open_settings_window(&app) } +#[tauri::command] +fn close_settings_window(app: AppHandle) -> Result<(), String> { + if let Some(window) = app.get_webview_window("settings") { + window.close().map_err(|e| format!("Failed to close settings window: {e}"))?; + } + Ok(()) +} + +/// Fetch JSON from Polymarket Gamma API using native TLS (bypasses Cloudflare JA3 blocking). +/// Called from frontend when browser CORS and sidecar Node.js TLS both fail. +#[tauri::command] +async fn fetch_polymarket(path: String, params: String) -> Result { + let allowed = ["events", "markets", "tags"]; + let segment = path.trim_start_matches('/'); + if !allowed.iter().any(|a| segment.starts_with(a)) { + return Err("Invalid Polymarket path".into()); + } + let url = format!("https://gamma-api.polymarket.com/{}?{}", segment, params); + let client = reqwest::Client::builder() + .use_native_tls() + .build() + .map_err(|e| format!("HTTP client error: {e}"))?; + let resp = client + .get(&url) + .header("Accept", "application/json") + .timeout(std::time::Duration::from_secs(10)) + .send() + .await + .map_err(|e| format!("Polymarket fetch failed: {e}"))?; + if !resp.status().is_success() { + return Err(format!("Polymarket HTTP {}", resp.status())); + } + resp.text().await.map_err(|e| format!("Read body failed: {e}")) +} + fn open_settings_window(app: &AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("settings") { let _ = window.show(); @@ -261,28 +307,31 @@ fn build_app_menu(handle: &AppHandle) -> tauri::Result> { let file_menu = Submenu::with_items(handle, "File", true, &[&settings_item, &separator, &quit_item])?; - let open_logs_item = MenuItem::with_id( + let about_metadata = AboutMetadata { + name: Some("World Monitor".into()), + version: Some(env!("CARGO_PKG_VERSION").into()), + copyright: Some("\u{00a9} 2025 Elie Habib".into()), + website: Some("https://worldmonitor.app".into()), + website_label: Some("worldmonitor.app".into()), + ..Default::default() + }; + let about_item = PredefinedMenuItem::about(handle, Some("About World Monitor"), Some(about_metadata))?; + let github_item = MenuItem::with_id( handle, - MENU_DEBUG_OPEN_LOGS_ID, - "Open Logs Folder", + MENU_HELP_GITHUB_ID, + "GitHub Repository", true, None::<&str>, )?; - let open_sidecar_log_item = MenuItem::with_id( + let help_separator = PredefinedMenuItem::separator(handle)?; + let help_menu = Submenu::with_items( handle, - MENU_DEBUG_OPEN_SIDECAR_LOG_ID, - "Open Local API Log", + "Help", true, - None::<&str>, - )?; - let debug_menu = Submenu::with_items( - handle, - "Debug", - true, - &[&open_logs_item, &open_sidecar_log_item], + &[&about_item, &help_separator, &github_item], )?; - Menu::with_items(handle, &[&file_menu, &debug_menu]) + Menu::with_items(handle, &[&file_menu, &help_menu]) } fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { @@ -293,17 +342,8 @@ fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) { eprintln!("[tauri] settings menu failed: {err}"); } } - MENU_DEBUG_OPEN_LOGS_ID => { - if let Err(err) = open_logs_folder_impl(app) { - append_desktop_log(app, "ERROR", &format!("open logs folder failed: {err}")); - eprintln!("[tauri] open logs folder failed: {err}"); - } - } - MENU_DEBUG_OPEN_SIDECAR_LOG_ID => { - if let Err(err) = open_sidecar_log_impl(app) { - append_desktop_log(app, "ERROR", &format!("open sidecar log failed: {err}")); - eprintln!("[tauri] open sidecar log failed: {err}"); - } + MENU_HELP_GITHUB_ID => { + let _ = open_in_shell("https://github.com/koala73/worldmonitor"); } _ => {} } @@ -428,6 +468,20 @@ fn start_local_api(app: &AppHandle) -> Result<(), String> { .stdout(Stdio::from(log_file)) .stderr(Stdio::from(log_file_err)); + // Pass keychain secrets to sidecar as env vars + let mut secret_count = 0u32; + for key in SUPPORTED_SECRET_KEYS.iter() { + if let Ok(entry) = Entry::new(KEYRING_SERVICE, key) { + if let Ok(value) = entry.get_password() { + if !value.trim().is_empty() { + cmd.env(key, value.trim()); + secret_count += 1; + } + } + } + } + append_desktop_log(app, "INFO", &format!("injected {secret_count} keychain secrets into sidecar env")); + let child = cmd .spawn() .map_err(|e| format!("Failed to launch local API: {e}"))?; @@ -461,7 +515,10 @@ fn main() { write_cache_entry, open_logs_folder, open_sidecar_log_file, - open_settings_window_command + open_settings_window_command, + close_settings_window, + open_url, + fetch_polymarket ]) .setup(|app| { if let Err(err) = start_local_api(&app.handle()) { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 34087deec..a943c6dd5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "World Monitor", "mainBinaryName": "world-monitor", - "version": "0.1.0", + "version": "2.2.0", "identifier": "app.worldmonitor.desktop", "build": { "beforeDevCommand": "npm run dev", @@ -23,7 +23,7 @@ } ], "security": { - "csp": "default-src 'self'; connect-src 'self' https: http://localhost:5173 http://127.0.0.1:46123 ws: wss: blob: data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://www.youtube.com; worker-src 'self' blob:; font-src 'self' data: https:; media-src 'self' data: blob: https:; frame-src 'self' http://127.0.0.1:46123 https://www.youtube.com https://www.youtube-nocookie.com;" + "csp": "default-src 'self'; connect-src 'self' https: http://localhost:5173 http://127.0.0.1:46123 ws: wss: blob: data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://www.youtube.com; worker-src 'self' blob:; font-src 'self' data: https:; media-src 'self' data: blob: https:; frame-src 'self' http://127.0.0.1:46123 https://worldmonitor.app https://tech.worldmonitor.app https://www.youtube.com https://www.youtube-nocookie.com;" } }, "bundle": { diff --git a/src/App.ts b/src/App.ts index 65378713e..d32763004 100644 --- a/src/App.ts +++ b/src/App.ts @@ -87,6 +87,7 @@ import { AI_RESEARCH_LABS } from '@/config/ai-research-labs'; import { STARTUP_ECOSYSTEMS } from '@/config/startup-ecosystems'; import { TECH_HQS, ACCELERATORS } from '@/config/tech-geo'; import { isDesktopRuntime } from '@/services/runtime'; + import type { PredictionMarket, MarketData, ClusteredEvent } from '@/types'; export class App { @@ -556,13 +557,53 @@ export class App { context.stockIndex = `${stockData.indexName}: ${stockData.price} (${pct >= 0 ? '+' : ''}${stockData.weekChangePercent}% week)`; } - const res = await fetch('/api/country-intel', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ country: geo.country, code: geo.code, context }), - }); - const data = await res.json(); - this.countryIntelModal!.updateBrief({ ...data, code: geo.code }); + let data: Record | null = null; + try { + const res = await fetch('/api/country-intel', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ country: geo.country, code: geo.code, context }), + }); + data = await res.json(); + } catch { /* server unreachable — will fall through to browser fallback */ } + + if (data && data.brief && !data.skipped) { + this.countryIntelModal!.updateBrief({ ...data, code: geo.code } as Parameters[0]); + } else { + // Fallback: generate a basic brief from headlines using browser T5 + const briefHeadlines = (context.headlines as string[] | undefined) || []; + let fallbackBrief = ''; + if (briefHeadlines.length >= 2 && mlWorker.isAvailable) { + try { + const prompt = `Summarize the current situation in ${geo.country} based on these headlines: ${briefHeadlines.slice(0, 8).join('. ')}`; + const [summary] = await mlWorker.summarize([prompt]); + if (summary && summary.length > 20) fallbackBrief = summary; + } catch { /* T5 failed */ } + } + + if (fallbackBrief) { + this.countryIntelModal!.updateBrief({ brief: fallbackBrief, country: geo.country, code: geo.code, fallback: true }); + } else { + // Build a data-only brief from available context + const lines: string[] = []; + if (score) lines.push(`**Instability Index: ${score.score}/100** (${score.level}, ${score.trend})`); + if (signals.protests > 0) lines.push(`${signals.protests} active protests detected`); + if (signals.militaryFlights > 0) lines.push(`${signals.militaryFlights} military aircraft tracked`); + if (signals.militaryVessels > 0) lines.push(`${signals.militaryVessels} military vessels tracked`); + if (signals.outages > 0) lines.push(`${signals.outages} internet outages`); + if (signals.earthquakes > 0) lines.push(`${signals.earthquakes} recent earthquakes`); + if (context.stockIndex) lines.push(`Stock index: ${context.stockIndex}`); + if (briefHeadlines.length > 0) { + lines.push('', '**Recent headlines:**'); + briefHeadlines.slice(0, 5).forEach(h => lines.push(`• ${h}`)); + } + if (lines.length > 0) { + this.countryIntelModal!.updateBrief({ brief: lines.join('\n'), country: geo.country, code: geo.code, fallback: true }); + } else { + this.countryIntelModal!.updateBrief({ brief: '', country: geo.country, code: geo.code, error: 'No AI service available. Configure GROQ_API_KEY in Settings for full briefs.' }); + } + } + } } catch (err) { console.error('[CountryIntel] fetch error:', err); this.countryIntelModal!.updateBrief({ brief: '', country: geo.country, code: geo.code, error: 'Failed to generate brief' }); @@ -1107,20 +1148,20 @@ export class App {
- + data-variant="full" + ${!this.isDesktopApp && SITE_VARIANT === 'tech' ? 'target="_blank" rel="noopener"' : ''} + title="Geopolitical Intelligence${SITE_VARIANT !== 'tech' ? ' (current)' : ''}"> 🌍 WORLD - + ${!this.isDesktopApp && SITE_VARIANT !== 'tech' ? 'target="_blank" rel="noopener"' : ''} + title="Tech & AI Intelligence${SITE_VARIANT === 'tech' ? ' (current)' : ''}"> 💻 TECH @@ -1838,6 +1879,20 @@ export class App { // Sources modal this.setupSourcesModal(); + // Variant switcher: switch variant locally on desktop (reload with new config) + if (this.isDesktopApp) { + this.container.querySelectorAll('.variant-option').forEach(link => { + link.addEventListener('click', (e) => { + const variant = link.dataset.variant; + if (variant && variant !== SITE_VARIANT) { + e.preventDefault(); + localStorage.setItem('worldmonitor-variant', variant); + window.location.reload(); + } + }); + }); + } + // Fullscreen toggle const fullscreenBtn = document.getElementById('fullscreenBtn'); if (!this.isDesktopApp && fullscreenBtn) { diff --git a/src/components/CountryIntelModal.ts b/src/components/CountryIntelModal.ts index 855771100..5df4a0c09 100644 --- a/src/components/CountryIntelModal.ts +++ b/src/components/CountryIntelModal.ts @@ -188,14 +188,15 @@ export class CountryIntelModal { this.overlay.classList.add('active'); } - public updateBrief(data: CountryIntelData): void { + public updateBrief(data: CountryIntelData & { skipped?: boolean; reason?: string; fallback?: boolean }): void { if (data.code !== this.currentCode) return; const briefSection = this.contentEl.querySelector('.intel-brief-section'); if (!briefSection) return; - if (data.error) { - briefSection.innerHTML = `
Unable to generate brief. ${escapeHtml(data.error)}
`; + if (data.error || data.skipped || !data.brief) { + const msg = data.error || data.reason || 'AI brief unavailable — configure GROQ_API_KEY in Settings.'; + briefSection.innerHTML = `
${escapeHtml(msg)}
`; return; } diff --git a/src/components/DeckGLMap.ts b/src/components/DeckGLMap.ts index 9a1252e6a..a930d164d 100644 --- a/src/components/DeckGLMap.ts +++ b/src/components/DeckGLMap.ts @@ -5,8 +5,9 @@ */ import { MapboxOverlay } from '@deck.gl/mapbox'; import type { Layer, LayersList, PickingInfo } from '@deck.gl/core'; -import { GeoJsonLayer, ScatterplotLayer, PathLayer, IconLayer } from '@deck.gl/layers'; +import { GeoJsonLayer, ScatterplotLayer, PathLayer, IconLayer, TextLayer } from '@deck.gl/layers'; import maplibregl from 'maplibre-gl'; +import Supercluster from 'supercluster'; import type { MapLayers, Hotspot, @@ -20,6 +21,7 @@ import type { CableAdvisory, RepairShip, SocialUnrestEvent, + AIDataCenter, AirportDelayAlert, MilitaryFlight, MilitaryVessel, @@ -29,12 +31,16 @@ import type { UcdpGeoEvent, DisplacementFlow, ClimateAnomaly, + MapProtestCluster, + MapTechHQCluster, + MapTechEventCluster, + MapDatacenterCluster, } from '@/types'; import { ArcLayer } from '@deck.gl/layers'; import { HeatmapLayer } from '@deck.gl/aggregation-layers'; import type { WeatherAlert } from '@/services/weather'; import { escapeHtml } from '@/utils/sanitize'; -import { throttle, debounce, rafSchedule } from '@/utils/index'; +import { debounce, rafSchedule } from '@/utils/index'; import { INTEL_HOTSPOTS, CONFLICT_ZONES, @@ -113,7 +119,7 @@ const MAP_INTERACTION_MODE: MapInteractionMode = import.meta.env.VITE_MAP_INTERACTION_MODE === 'flat' ? 'flat' : '3d'; // Zoom thresholds for layer visibility and labels (matches old Map.ts) -// Used in renderClusterOverlays for zoom-dependent label visibility +// Zoom-dependent layer visibility and labels const LAYER_ZOOM_THRESHOLDS: Partial> = { bases: { minZoom: 3, showLabels: 5 }, nuclear: { minZoom: 3 }, @@ -228,26 +234,30 @@ export class DeckGLMap { private layerCache: Map = new Map(); private lastZoomThreshold = 0; - private clusterElementCache: Map = new Map(); - private lastClusterState: Map = new Map(); - private clusterResultCache: Map> = new Map(); - private lastClusterZoom = -1; + private protestSC: Supercluster | null = null; + private techHQSC: Supercluster | null = null; + private techEventSC: Supercluster | null = null; + private datacenterSC: Supercluster | null = null; + private protestClusters: MapProtestCluster[] = []; + private techHQClusters: MapTechHQCluster[] = []; + private techEventClusters: MapTechEventCluster[] = []; + private datacenterClusters: MapDatacenterCluster[] = []; + private lastSCZoom = -1; private newsPulseIntervalId: ReturnType | null = null; private lastCableHighlightSignature = ''; private lastPipelineHighlightSignature = ''; - private throttledRenderClusters: () => void; private debouncedRebuildLayers: () => void; private rafUpdateLayers: () => void; private moveTimeoutId: ReturnType | null = null; - private hotspotSyncRafId: number | null = null; - private hotspotOverlayNeedsReconcile = false; constructor(container: HTMLElement, initialState: DeckMapState) { this.container = container; this.state = initialState; this.hotspots = [...INTEL_HOTSPOTS]; - this.throttledRenderClusters = throttle(() => this.renderClusterOverlays(), 16); + this.rebuildTechHQSupercluster(); + this.rebuildDatacenterSupercluster(); + this.debouncedRebuildLayers = debounce(() => { this.maplibreMap?.resize(); this.deckOverlay?.setProps({ layers: this.buildLayers() }); @@ -276,9 +286,6 @@ export class DeckGLMap { this.createTimestamp(); } - // Cluster overlay container - private clusterOverlay: HTMLElement | null = null; - private setupDOM(): void { const wrapper = document.createElement('div'); wrapper.className = 'deckgl-map-wrapper'; @@ -291,12 +298,6 @@ export class DeckGLMap { mapContainer.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;'; wrapper.appendChild(mapContainer); - // HTML overlay container for cluster markers (rendered on top of deck.gl) - this.clusterOverlay = document.createElement('div'); - this.clusterOverlay.id = 'deckgl-cluster-overlay'; - this.clusterOverlay.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10;'; - wrapper.appendChild(this.clusterOverlay); - this.container.appendChild(wrapper); } @@ -372,25 +373,27 @@ export class DeckGLMap { clearTimeout(this.moveTimeoutId); this.moveTimeoutId = null; } - this.clusterOverlay!.style.opacity = '0.7'; }); this.maplibreMap.on('moveend', () => { - this.clusterOverlay!.style.opacity = '1'; - this.scheduleHotspotOverlaySync({ reconcile: true }); - this.throttledRenderClusters(); + this.lastSCZoom = -1; + this.rafUpdateLayers(); }); this.maplibreMap.on('move', () => { - this.scheduleHotspotOverlaySync(); if (this.moveTimeoutId) clearTimeout(this.moveTimeoutId); - this.moveTimeoutId = setTimeout(() => this.throttledRenderClusters(), 100); + this.moveTimeoutId = setTimeout(() => { + this.lastSCZoom = -1; + this.rafUpdateLayers(); + }, 100); }); this.maplibreMap.on('zoom', () => { - this.scheduleHotspotOverlaySync(); if (this.moveTimeoutId) clearTimeout(this.moveTimeoutId); - this.moveTimeoutId = setTimeout(() => this.throttledRenderClusters(), 100); + this.moveTimeoutId = setTimeout(() => { + this.lastSCZoom = -1; + this.rafUpdateLayers(); + }, 100); }); this.maplibreMap.on('zoomend', () => { @@ -404,90 +407,14 @@ export class DeckGLMap { } private setupResizeObserver(): void { - // Watch container for size changes and trigger MapLibre resize this.resizeObserver = new ResizeObserver(() => { if (this.maplibreMap) { this.maplibreMap.resize(); - this.renderClusterOverlays(); } }); this.resizeObserver.observe(this.container); } - // Generic marker clustering - groups markers within pixelRadius into clusters - // groupKey function ensures only items with same key can cluster (e.g., same city) - private clusterMarkers( - items: T[], - pixelRadius: number, - getGroupKey?: (item: T) => string - ): Array<{ items: T[]; center: [number, number]; screenPos: [number, number] }> { - if (!this.maplibreMap) return []; - - const clusters: Array<{ items: T[]; center: [number, number]; screenPos: [number, number] }> = []; - const assigned = new Set(); - - for (let i = 0; i < items.length; i++) { - if (assigned.has(i)) continue; - - const item = items[i]!; - const itemLon = item.lon ?? item.lng; - if (!Number.isFinite(item.lat) || !Number.isFinite(itemLon)) continue; - const pos = this.maplibreMap.project([itemLon!, item.lat]); - if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) continue; - - const cluster: T[] = [item]; - assigned.add(i); - const itemKey = getGroupKey?.(item); - - // Find nearby items (must share same group key if provided) - for (let j = i + 1; j < items.length; j++) { - if (assigned.has(j)) continue; - - const other = items[j]!; - const otherKey = getGroupKey?.(other); - - // Skip if group keys don't match - if (itemKey !== undefined && otherKey !== undefined && itemKey !== otherKey) continue; - - const otherLon = other.lon ?? other.lng; - if (!Number.isFinite(other.lat) || !Number.isFinite(otherLon)) continue; - const otherPos = this.maplibreMap.project([otherLon!, other.lat]); - if (!Number.isFinite(otherPos.x) || !Number.isFinite(otherPos.y)) continue; - - const dist = Math.sqrt( - Math.pow(pos.x - otherPos.x, 2) + Math.pow(pos.y - otherPos.y, 2) - ); - - if (dist <= pixelRadius) { - cluster.push(other); - assigned.add(j); - } - } - - // Calculate cluster center - let sumLat = 0, sumLon = 0; - for (const c of cluster) { - sumLat += c.lat; - sumLon += c.lon ?? c.lng ?? 0; // safe: items already validated - } - const centerLat = sumLat / cluster.length; - const centerLon = sumLon / cluster.length; - const centerPos = this.maplibreMap.project([centerLon, centerLat]); - const validCenter = centerPos && Number.isFinite(centerPos.x) && Number.isFinite(centerPos.y); - - clusters.push({ - items: cluster, - center: [centerLon, centerLat], - screenPos: validCenter ? [centerPos.x, centerPos.y] : [pos.x, pos.y], - }); - } - - return clusters; - } - - private getClusterKey(type: string, center: [number, number], count: number): string { - return `${type}-${center[0].toFixed(4)}-${center[1].toFixed(4)}-${count}`; - } private getSetSignature(set: Set): string { return [...set].sort().join('|'); @@ -500,501 +427,166 @@ export class DeckGLMap { return false; } - private projectClusterCenter( - center: [number, number], - fallback: [number, number] - ): [number, number] { - if (!this.maplibreMap) return fallback; - if (!Number.isFinite(center[0]) || !Number.isFinite(center[1])) return fallback; - const projected = this.maplibreMap.project(center); - if (!Number.isFinite(projected.x) || !Number.isFinite(projected.y)) return fallback; - return [projected.x, projected.y]; + private rebuildProtestSupercluster(): void { + const points = this.protests.map((p, i) => ({ + type: 'Feature' as const, + geometry: { type: 'Point' as const, coordinates: [p.lon, p.lat] as [number, number] }, + properties: { index: i }, + })); + this.protestSC = new Supercluster({ radius: 60, maxZoom: 14 }); + this.protestSC.load(points); + this.lastSCZoom = -1; } - private updateClusterElement( - key: string, - screenPos: [number, number], - renderFn: () => HTMLElement - ): HTMLElement { - const existing = this.clusterElementCache.get(key); - const x = Math.round(screenPos[0]); - const y = Math.round(screenPos[1]); - - if (existing) { - existing.style.left = `${x}px`; - existing.style.top = `${y}px`; - return existing; - } - - const element = renderFn(); - element.style.position = 'absolute'; - element.style.left = `${x}px`; - element.style.top = `${y}px`; - element.dataset.clusterKey = key; - this.clusterElementCache.set(key, element); - return element; + private rebuildTechHQSupercluster(): void { + const points = TECH_HQS.map((h, i) => ({ + type: 'Feature' as const, + geometry: { type: 'Point' as const, coordinates: [h.lon, h.lat] as [number, number] }, + properties: { index: i }, + })); + this.techHQSC = new Supercluster({ radius: 50, maxZoom: 14 }); + this.techHQSC.load(points); + this.lastSCZoom = -1; } - private pruneClusterCache(activeKeys: Set): void { - for (const [key, element] of this.clusterElementCache) { - if (!activeKeys.has(key)) { - element.remove(); - this.clusterElementCache.delete(key); - this.lastClusterState.delete(key); - } - } + private rebuildTechEventSupercluster(): void { + const points = this.techEvents.map((e, i) => ({ + type: 'Feature' as const, + geometry: { type: 'Point' as const, coordinates: [e.lng, e.lat] as [number, number] }, + properties: { index: i }, + })); + this.techEventSC = new Supercluster({ radius: 50, maxZoom: 14 }); + this.techEventSC.load(points); + this.lastSCZoom = -1; } - private invalidateClusterElementsByType(type: 'event' | 'protest'): void { - const prefix = `${type}-`; - for (const [key, element] of this.clusterElementCache) { - if (key.startsWith(prefix)) { - element.remove(); - this.clusterElementCache.delete(key); - } - } - for (const key of this.lastClusterState.keys()) { - if (key.startsWith(prefix)) { - this.lastClusterState.delete(key); - } - } + private rebuildDatacenterSupercluster(): void { + const activeDCs = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned'); + const points = activeDCs.map((dc, i) => ({ + type: 'Feature' as const, + geometry: { type: 'Point' as const, coordinates: [dc.lon, dc.lat] as [number, number] }, + properties: { index: i }, + })); + this.datacenterSC = new Supercluster({ radius: 70, maxZoom: 14 }); + this.datacenterSC.load(points); + this.lastSCZoom = -1; } - private renderClusterOverlays(): void { - if (!this.clusterOverlay || !this.maplibreMap) return; + private updateClusterData(): void { + const zoom = Math.floor(this.maplibreMap?.getZoom() ?? 2); + if (zoom === this.lastSCZoom) return; + this.lastSCZoom = zoom; - const startTime = performance.now(); - const zoom = this.maplibreMap.getZoom(); - const zoomChanged = Math.abs(zoom - this.lastClusterZoom) > 0.1; - if (zoomChanged) { - this.clusterResultCache.clear(); - this.lastClusterZoom = zoom; - } + const bounds = this.maplibreMap?.getBounds(); + if (!bounds) return; + const bbox: [number, number, number, number] = [ + bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth(), + ]; - const activeKeys = new Set(); - - if (SITE_VARIANT === 'tech') { - if (this.state.layers.techHQs) { - const clusterRadius = zoom >= 4 ? 15 : zoom >= 3 ? 25 : 40; - const cacheKey = `hq-${clusterRadius}`; - let clusters = this.clusterResultCache.get(cacheKey) as Array<{ items: typeof TECH_HQS; center: [number, number]; screenPos: [number, number] }> | undefined; - if (!clusters || zoomChanged) { - clusters = this.clusterMarkers(TECH_HQS, clusterRadius, hq => hq.city); - this.clusterResultCache.set(cacheKey, clusters); - } - clusters.forEach((cluster) => { - const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos); - const key = this.getClusterKey('hq', cluster.center, cluster.items.length); - activeKeys.add(key); - if (this.hasClusterMoved(key, screenPos, cluster.items.length)) { - const element = this.updateClusterElement(key, screenPos, () => this.createTechHQClusterElement(cluster)); - if (!element.parentElement) this.clusterOverlay!.appendChild(element); - } - }); - } - - if (this.state.layers.techEvents && this.techEvents.length > 0) { - const clusterRadius = zoom >= 4 ? 15 : zoom >= 3 ? 25 : 40; - const cacheKey = `event-${clusterRadius}`; - let clusters = this.clusterResultCache.get(cacheKey) as Array<{ items: Array; center: [number, number]; screenPos: [number, number] }> | undefined; - if (!clusters || zoomChanged) { - const eventsWithLon = this.techEvents.map(e => ({ ...e, lon: e.lng })); - clusters = this.clusterMarkers(eventsWithLon, clusterRadius, e => e.location); - this.clusterResultCache.set(cacheKey, clusters); - } - clusters.forEach((cluster) => { - const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos); - const key = this.getClusterKey('event', cluster.center, cluster.items.length); - activeKeys.add(key); - if (this.hasClusterMoved(key, screenPos, cluster.items.length)) { - const element = this.updateClusterElement(key, screenPos, () => this.createTechEventClusterElement(cluster)); - if (!element.parentElement) this.clusterOverlay!.appendChild(element); - } - }); - } - } - - if (this.state.layers.protests && this.protests.length > 0) { - const clusterRadius = zoom >= 4 ? 12 : zoom >= 3 ? 20 : 35; - const cacheKey = `protest-${clusterRadius}`; - let clusters = this.clusterResultCache.get(cacheKey) as Array<{ items: SocialUnrestEvent[]; center: [number, number]; screenPos: [number, number] }> | undefined; - if (!clusters || zoomChanged) { - const significantProtests = this.protests.filter(p => p.severity === 'high' || p.eventType === 'riot'); - clusters = this.clusterMarkers(significantProtests, clusterRadius, p => p.country); - this.clusterResultCache.set(cacheKey, clusters); - } - clusters.forEach((cluster) => { - const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos); - const key = this.getClusterKey('protest', cluster.center, cluster.items.length); - activeKeys.add(key); - if (this.hasClusterMoved(key, screenPos, cluster.items.length)) { - const element = this.updateClusterElement(key, screenPos, () => this.createProtestClusterElement(cluster)); - if (!element.parentElement) this.clusterOverlay!.appendChild(element); + if (this.protestSC) { + this.protestClusters = this.protestSC.getClusters(bbox, zoom).map(f => { + const coords = f.geometry.coordinates as [number, number]; + if (f.properties.cluster) { + const leaves = this.protestSC!.getLeaves(f.properties.cluster_id!, Infinity); + const items = leaves.map(l => this.protests[l.properties.index]).filter((x): x is SocialUnrestEvent => !!x); + const maxSev = items.some(i => i.severity === 'high') ? 'high' : items.some(i => i.severity === 'medium') ? 'medium' : 'low'; + return { + id: `pc-${f.properties.cluster_id}`, + lat: coords[1], lon: coords[0], + count: f.properties.point_count!, + items, country: items[0]?.country ?? '', + maxSeverity: maxSev as 'low' | 'medium' | 'high', + hasRiot: items.some(i => i.eventType === 'riot'), + totalFatalities: items.reduce((s, i) => s + (i.fatalities ?? 0), 0), + }; } + const item = this.protests[f.properties.index]!; + return { + id: `pp-${f.properties.index}`, lat: item.lat, lon: item.lon, + count: 1, items: [item], country: item.country, + maxSeverity: item.severity, hasRiot: item.eventType === 'riot', + totalFatalities: item.fatalities ?? 0, + }; }); } - if (this.state.layers.datacenters && zoom < 5) { - const clusterRadius = zoom >= 3 ? 30 : zoom >= 2 ? 50 : 70; - const cacheKey = `dc-${clusterRadius}`; - let clusters = this.clusterResultCache.get(cacheKey) as Array<{ items: typeof AI_DATA_CENTERS; center: [number, number]; screenPos: [number, number] }> | undefined; - if (!clusters || zoomChanged) { - const activeDCs = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned'); - clusters = this.clusterMarkers(activeDCs, clusterRadius, dc => dc.country); - this.clusterResultCache.set(cacheKey, clusters); - } - clusters.forEach((cluster) => { - const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos); - const key = this.getClusterKey('dc', cluster.center, cluster.items.length); - activeKeys.add(key); - if (this.hasClusterMoved(key, screenPos, cluster.items.length)) { - const element = this.updateClusterElement(key, screenPos, () => this.createDatacenterClusterElement(cluster)); - if (!element.parentElement) this.clusterOverlay!.appendChild(element); + if (this.techHQSC) { + this.techHQClusters = this.techHQSC.getClusters(bbox, zoom).map(f => { + const coords = f.geometry.coordinates as [number, number]; + if (f.properties.cluster) { + const leaves = this.techHQSC!.getLeaves(f.properties.cluster_id!, Infinity); + const items = leaves.map(l => TECH_HQS[l.properties.index]).filter(Boolean) as typeof TECH_HQS; + return { + id: `hc-${f.properties.cluster_id}`, + lat: coords[1], lon: coords[0], + count: f.properties.point_count!, + items, city: items[0]?.city ?? '', country: items[0]?.country ?? '', + primaryType: items[0]?.type ?? 'public', + }; } + const item = TECH_HQS[f.properties.index]!; + return { + id: `hp-${f.properties.index}`, lat: item.lat, lon: item.lon, + count: 1, items: [item], city: item.city, country: item.country, + primaryType: item.type, + }; }); } - if (this.state.layers.hotspots) { - this.renderHotspotOverlays(activeKeys); - } - - this.pruneClusterCache(activeKeys); - - const elapsed = performance.now() - startTime; - if (import.meta.env.DEV && elapsed > 16) { - console.warn(`[DeckGLMap] renderClusterOverlays took ${elapsed.toFixed(2)}ms (>16ms budget)`); - } - } - - private hasClusterMoved(key: string, screenPos: [number, number], count: number): boolean { - const last = this.lastClusterState.get(key); - if (!last) { - this.lastClusterState.set(key, { x: screenPos[0], y: screenPos[1], count }); - return true; - } - const dx = Math.abs(last.x - screenPos[0]); - const dy = Math.abs(last.y - screenPos[1]); - const moved = dx > 5 || dy > 5 || last.count !== count; - if (moved) { - this.lastClusterState.set(key, { x: screenPos[0], y: screenPos[1], count }); - } - return moved; - } - - /** Render HTML overlays for high-activity hotspots with CSS pulsating animation */ - private renderHotspotOverlays(activeKeys: Set): void { - if (!this.clusterOverlay || !this.maplibreMap) return; - - const zoom = this.maplibreMap.getZoom(); - const highActivityHotspots = this.hotspots.filter(h => h.level === 'high' || h.hasBreaking); - - const sorted = [...highActivityHotspots].sort( - (a, b) => (b.escalationScore || 0) - (a.escalationScore || 0) - ); - - const markerScale = zoom < 2.5 ? 0.7 : zoom < 4 ? 0.85 : 1.0; - - sorted.forEach(hotspot => { - const pos = this.maplibreMap!.project([hotspot.lon, hotspot.lat]); - if (!pos) return; - - const key = this.getClusterKey('hotspot', [hotspot.lon, hotspot.lat], 1); - activeKeys.add(key); - const existing = this.clusterElementCache.get(key); - - if (existing) { - existing.style.transform = `translate(${pos.x - 16}px, ${pos.y - 16}px) scale(${markerScale})`; - if (!existing.parentElement) this.clusterOverlay!.appendChild(existing); - return; - } - - const div = document.createElement('div'); - div.className = 'hotspot'; - div.style.cssText = `position: absolute; left: 0; top: 0; transform: translate(${pos.x - 16}px, ${pos.y - 16}px) scale(${markerScale}); pointer-events: auto; cursor: pointer; z-index: 100;`; - div.dataset.clusterKey = key; - - div.innerHTML = ` -
- `; - - div.addEventListener('click', (e) => { - e.stopPropagation(); - const relatedNews = this.getRelatedNews(hotspot); - const rect = this.container.getBoundingClientRect(); - this.popup.show({ - type: 'hotspot', - data: hotspot, - relatedNews, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - this.popup.loadHotspotGdeltContext(hotspot); - this.onHotspotClick?.(hotspot); + if (this.techEventSC) { + this.techEventClusters = this.techEventSC.getClusters(bbox, zoom).map(f => { + const coords = f.geometry.coordinates as [number, number]; + if (f.properties.cluster) { + const leaves = this.techEventSC!.getLeaves(f.properties.cluster_id!, Infinity); + const items = leaves.map(l => this.techEvents[l.properties.index]).filter((x): x is TechEventMarker => !!x); + return { + id: `ec-${f.properties.cluster_id}`, + lat: coords[1], lon: coords[0], + count: f.properties.point_count!, + items, location: items[0]?.location ?? '', country: items[0]?.country ?? '', + soonestDaysUntil: Math.min(...items.map(i => i.daysUntil)), + }; + } + const item = this.techEvents[f.properties.index]!; + return { + id: `ep-${f.properties.index}`, lat: item.lat, lon: item.lng, + count: 1, items: [item], location: item.location, country: item.country, + soonestDaysUntil: item.daysUntil, + }; }); + } - this.clusterElementCache.set(key, div); - this.clusterOverlay!.appendChild(div); - }); - } - - private updateHotspotPositions(): void { - if (!this.clusterOverlay || !this.maplibreMap || !this.state.layers.hotspots) return; - - const zoom = this.maplibreMap.getZoom(); - const markerScale = zoom < 2.5 ? 0.7 : zoom < 4 ? 0.85 : 1.0; - - for (const [key, existing] of this.clusterElementCache) { - if (!key.startsWith('hotspot-')) continue; - - const match = /^hotspot-(-?\d+\.\d+)-(-?\d+\.\d+)-\d+$/.exec(key); - if (!match) continue; - const lon = Number(match[1]); - const lat = Number(match[2]); - if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue; - - const pos = this.maplibreMap.project([lon, lat]); - if (!pos) continue; - - existing.style.transform = `translate(${pos.x - 16}px, ${pos.y - 16}px) scale(${markerScale})`; + if (this.datacenterSC) { + const activeDCs = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned'); + this.datacenterClusters = this.datacenterSC.getClusters(bbox, zoom).map(f => { + const coords = f.geometry.coordinates as [number, number]; + if (f.properties.cluster) { + const leaves = this.datacenterSC!.getLeaves(f.properties.cluster_id!, Infinity); + const items = leaves.map(l => activeDCs[l.properties.index]).filter((x): x is AIDataCenter => !!x); + const existingCount = items.filter(i => i.status === 'existing').length; + return { + id: `dc-${f.properties.cluster_id}`, + lat: coords[1], lon: coords[0], + count: f.properties.point_count!, + items, region: items[0]?.country ?? '', country: items[0]?.country ?? '', + totalChips: items.reduce((s, i) => s + i.chipCount, 0), + totalPowerMW: items.reduce((s, i) => s + (i.powerMW ?? 0), 0), + majorityExisting: existingCount >= items.length / 2, + }; + } + const item = activeDCs[f.properties.index]!; + return { + id: `dp-${f.properties.index}`, lat: item.lat, lon: item.lon, + count: 1, items: [item], region: item.country, country: item.country, + totalChips: item.chipCount, totalPowerMW: item.powerMW ?? 0, + majorityExisting: item.status === 'existing', + }; + }); } } - private scheduleHotspotOverlaySync(options?: { reconcile?: boolean }): void { - if (!this.state.layers.hotspots) return; - if (options?.reconcile) { - this.hotspotOverlayNeedsReconcile = true; - } - if (this.hotspotSyncRafId !== null) return; - - this.hotspotSyncRafId = requestAnimationFrame(() => { - this.hotspotSyncRafId = null; - - if (!this.clusterOverlay || !this.maplibreMap || !this.state.layers.hotspots) { - this.hotspotOverlayNeedsReconcile = false; - return; - } - - this.updateHotspotPositions(); - - if (this.hotspotOverlayNeedsReconcile) { - this.hotspotOverlayNeedsReconcile = false; - this.throttledRenderClusters(); - } - }); - } - - private createTechHQClusterElement(cluster: { items: typeof TECH_HQS; center: [number, number]; screenPos: [number, number] }): HTMLElement { - const zoom = this.maplibreMap?.getZoom() || 2; - const primaryItem = cluster.items[0]!; - const isCluster = cluster.items.length > 1; - const unicornCount = cluster.items.filter(h => h.type === 'unicorn').length; - const faangCount = cluster.items.filter(h => h.type === 'faang').length; - - const div = document.createElement('div'); - div.className = `tech-hq-marker ${primaryItem.type} ${isCluster ? 'cluster' : ''}`; - div.style.cssText = 'pointer-events: auto; cursor: pointer;'; - - const icon = document.createElement('div'); - icon.className = 'tech-hq-icon'; - icon.textContent = faangCount > 0 ? '🏛️' : unicornCount > 0 ? '🦄' : '🏢'; - div.appendChild(icon); - - if (isCluster) { - const badge = document.createElement('div'); - badge.className = 'cluster-badge'; - badge.textContent = String(cluster.items.length); - div.appendChild(badge); - div.title = cluster.items.map(h => h.company).join(', '); - } else { - if (zoom >= 3 || primaryItem.type === 'faang') { - const label = document.createElement('div'); - label.className = 'tech-hq-label'; - label.textContent = primaryItem.company; - div.appendChild(label); - } - div.title = primaryItem.company; - } - - div.addEventListener('click', (e) => { - e.stopPropagation(); - const rect = this.container.getBoundingClientRect(); - if (isCluster) { - this.popup.show({ - type: 'techHQCluster', - data: { items: cluster.items, city: primaryItem.city, country: primaryItem.country }, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } else { - this.popup.show({ - type: 'techHQ', - data: primaryItem, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } - }); - - return div; - } - - private createTechEventClusterElement(cluster: { items: Array; center: [number, number]; screenPos: [number, number] }): HTMLElement { - const primaryEvent = cluster.items[0]!; - const isCluster = cluster.items.length > 1; - const hasUpcomingSoon = cluster.items.some(e => e.daysUntil <= 14); - - const div = document.createElement('div'); - div.className = `tech-event-marker ${hasUpcomingSoon ? 'upcoming-soon' : ''} ${isCluster ? 'cluster' : ''}`; - div.style.cssText = 'pointer-events: auto; cursor: pointer;'; - - const icon = document.createElement('div'); - icon.className = 'tech-event-icon'; - icon.textContent = '📅'; - div.appendChild(icon); - - if (isCluster) { - const badge = document.createElement('div'); - badge.className = 'cluster-badge'; - badge.textContent = String(cluster.items.length); - div.appendChild(badge); - div.title = cluster.items.map(e => e.title).join(', '); - } else { - div.title = primaryEvent.title; - } - - div.addEventListener('click', (e) => { - e.stopPropagation(); - const rect = this.container.getBoundingClientRect(); - if (isCluster) { - this.popup.show({ - type: 'techEventCluster', - data: { items: cluster.items.map(({ lon, ...rest }) => rest), location: primaryEvent.location, country: primaryEvent.country }, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } else { - this.popup.show({ - type: 'techEvent', - data: primaryEvent, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } - }); - - return div; - } - - private createProtestClusterElement(cluster: { items: SocialUnrestEvent[]; center: [number, number]; screenPos: [number, number] }): HTMLElement { - const primaryEvent = cluster.items[0]!; - const isCluster = cluster.items.length > 1; - const hasRiot = cluster.items.some(e => e.eventType === 'riot'); - const hasHighSeverity = cluster.items.some(e => e.severity === 'high'); - - const div = document.createElement('div'); - div.className = `protest-marker ${hasHighSeverity ? 'high' : primaryEvent.severity} ${hasRiot ? 'riot' : primaryEvent.eventType} ${isCluster ? 'cluster' : ''}`; - div.style.cssText = 'pointer-events: auto; cursor: pointer;'; - - const icon = document.createElement('div'); - icon.className = 'protest-icon'; - icon.textContent = hasRiot ? '🔥' : primaryEvent.eventType === 'strike' ? '✊' : '✦'; - div.appendChild(icon); - - if (isCluster) { - const badge = document.createElement('div'); - badge.className = 'cluster-badge'; - badge.textContent = String(cluster.items.length); - div.appendChild(badge); - div.title = `${primaryEvent.country}: ${cluster.items.length} events`; - } else { - div.title = `${primaryEvent.city || primaryEvent.country} - ${primaryEvent.eventType} (${primaryEvent.severity})`; - if (primaryEvent.validated) { - div.classList.add('validated'); - } - } - - div.addEventListener('click', (e) => { - e.stopPropagation(); - const rect = this.container.getBoundingClientRect(); - if (isCluster) { - this.popup.show({ - type: 'protestCluster', - data: { items: cluster.items, country: primaryEvent.country }, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } else { - this.popup.show({ - type: 'protest', - data: primaryEvent, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } - }); - - return div; - } - - private createDatacenterClusterElement(cluster: { items: typeof AI_DATA_CENTERS; center: [number, number]; screenPos: [number, number] }): HTMLElement { - const zoom = this.maplibreMap?.getZoom() || 2; - const primaryDC = cluster.items[0]!; - const isCluster = cluster.items.length > 1; - const totalChips = cluster.items.reduce((sum, dc) => sum + dc.chipCount, 0); - const hasPlanned = cluster.items.some(dc => dc.status === 'planned'); - const hasExisting = cluster.items.some(dc => dc.status === 'existing'); - - const div = document.createElement('div'); - div.className = `datacenter-marker ${hasPlanned && !hasExisting ? 'planned' : 'existing'} ${isCluster ? 'cluster' : ''}`; - div.style.cssText = 'pointer-events: auto; cursor: pointer;'; - - const icon = document.createElement('div'); - icon.className = 'datacenter-icon'; - icon.textContent = '🖥️'; - div.appendChild(icon); - - if (isCluster) { - const badge = document.createElement('div'); - badge.className = 'cluster-badge'; - badge.textContent = String(cluster.items.length); - div.appendChild(badge); - - const formatNum = (n: number) => n >= 1000000 ? `${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(0)}K` : String(n); - div.title = `${cluster.items.length} data centers • ${formatNum(totalChips)} chips`; - } else { - if (zoom >= 4) { - const label = document.createElement('div'); - label.className = 'datacenter-label'; - label.textContent = primaryDC.owner.split(',')[0] || primaryDC.name.slice(0, 15); - div.appendChild(label); - } - div.title = primaryDC.name; - } - - div.addEventListener('click', (e) => { - e.stopPropagation(); - const rect = this.container.getBoundingClientRect(); - if (isCluster) { - this.popup.show({ - type: 'datacenterCluster', - data: { items: cluster.items, region: primaryDC.country, country: primaryDC.country }, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } else { - this.popup.show({ - type: 'datacenter', - data: primaryDC, - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - } - }); - - return div; - } private isLayerVisible(layerKey: keyof MapLayers): boolean { const threshold = LAYER_ZOOM_THRESHOLDS[layerKey]; @@ -1023,14 +615,16 @@ export class DeckGLMap { layers.push(this.createConflictZonesLayer()); } - // Military bases layer — hidden at low zoom (E: progressive disclosure) + // Military bases layer — hidden at low zoom (E: progressive disclosure) + ghost if (mapLayers.bases && this.isLayerVisible('bases')) { layers.push(this.createBasesLayer()); + layers.push(this.createGhostLayer('bases-layer', MILITARY_BASES, d => [d.lon, d.lat], { radiusMinPixels: 12 })); } - // Nuclear facilities layer — hidden at low zoom + // Nuclear facilities layer — hidden at low zoom + ghost if (mapLayers.nuclear && this.isLayerVisible('nuclear')) { layers.push(this.createNuclearLayer()); + layers.push(this.createGhostLayer('nuclear-layer', NUCLEAR_FACILITIES.filter(f => f.status !== 'decommissioned'), d => [d.lon, d.lat], { radiusMinPixels: 12 })); } // Gamma irradiators layer — hidden at low zoom @@ -1043,20 +637,25 @@ export class DeckGLMap { layers.push(this.createSpaceportsLayer()); } - // Hotspots layer + // Hotspots layer (all hotspots including high/breaking, with pulse + ghost) if (mapLayers.hotspots) { - layers.push(this.createHotspotsLayer()); + layers.push(...this.createHotspotsLayers()); } - // Datacenters layer - SQUARE icons (only at high zoom, clusters handle low zoom) + // Datacenters layer - SQUARE icons at zoom >= 5, cluster dots at zoom < 5 const currentZoom = this.maplibreMap?.getZoom() || 2; - if (mapLayers.datacenters && currentZoom >= 5) { - layers.push(this.createDatacentersLayer()); + if (mapLayers.datacenters) { + if (currentZoom >= 5) { + layers.push(this.createDatacentersLayer()); + } else { + layers.push(...this.createDatacenterClusterLayers()); + } } - // Earthquakes layer + // Earthquakes layer + ghost for easier picking if (mapLayers.natural && this.earthquakes.length > 0) { layers.push(this.createEarthquakesLayer()); + layers.push(this.createGhostLayer('earthquakes-layer', this.earthquakes, d => [d.lon, d.lat], { radiusMinPixels: 12 })); } // Natural events layer @@ -1074,9 +673,10 @@ export class DeckGLMap { layers.push(this.createWeatherLayer()); } - // Internet outages layer + // Internet outages layer + ghost for easier picking if (mapLayers.outages && this.outages.length > 0) { layers.push(this.createOutagesLayer()); + layers.push(this.createGhostLayer('outages-layer', this.outages, d => [d.lon, d.lat], { radiusMinPixels: 12 })); } // AIS density layer @@ -1109,7 +709,10 @@ export class DeckGLMap { layers.push(this.createFlightDelaysLayer()); } - // Protests layer - rendered via HTML overlays in renderClusterOverlays() for clustering support + // Protests layer (Supercluster-based deck.gl layers) + if (mapLayers.protests && this.protests.length > 0) { + layers.push(...this.createProtestClusterLayers()); + } // Military vessels layer if (mapLayers.military && this.militaryVessels.length > 0) { @@ -1166,20 +769,23 @@ export class DeckGLMap { layers.push(this.createClimateHeatmapLayer()); } - // Tech variant layers - // Note: techHQs and techEvents are rendered via HTML overlays for clustering support + // Tech variant layers (Supercluster-based deck.gl layers for HQs and events) if (SITE_VARIANT === 'tech') { if (mapLayers.startupHubs) { layers.push(this.createStartupHubsLayer()); } - // techHQs rendered via HTML overlays in renderClusterOverlays() + if (mapLayers.techHQs) { + layers.push(...this.createTechHQClusterLayers()); + } if (mapLayers.accelerators) { layers.push(this.createAcceleratorsLayer()); } if (mapLayers.cloudRegions) { layers.push(this.createCloudRegionsLayer()); } - // techEvents rendered via HTML overlays in renderClusterOverlays() + if (mapLayers.techEvents && this.techEvents.length > 0) { + layers.push(...this.createTechEventClusterLayers()); + } } // News geo-locations (always shown if data exists) @@ -1425,40 +1031,19 @@ export class DeckGLMap { }); } - private createHotspotsLayer(): ScatterplotLayer { - const lowMediumHotspots = this.hotspots.filter(h => h.level !== 'high' && !h.hasBreaking); - const zoom = this.maplibreMap?.getZoom() || 2; - // B: Zoom-adaptive sizing — smaller at world view, full size zoomed in - const zoomScale = Math.min(1, (zoom - 1) / 3); // 0 at zoom 1, 1 at zoom 4 - const maxPx = 6 + Math.round(14 * zoomScale); // 6px at world, 20px zoomed - // F: Opacity falloff at low zoom - const baseOpacity = zoom < 2.5 ? 0.5 : zoom < 4 ? 0.7 : 1.0; - - return new ScatterplotLayer({ - id: 'hotspots-layer', - data: lowMediumHotspots, - getPosition: (d) => [d.lon, d.lat], - getRadius: (d) => { - const score = d.escalationScore || 1; - return 10000 + score * 5000; - }, - getFillColor: (d) => { - const score = d.escalationScore || 1; - const a = Math.round((score >= 4 ? 200 : score >= 2 ? 200 : 180) * baseOpacity); - if (score >= 4) return [255, 68, 68, a] as [number, number, number, number]; - if (score >= 2) return [255, 165, 0, a] as [number, number, number, number]; - return [255, 255, 0, a] as [number, number, number, number]; - }, - radiusMinPixels: 4, - radiusMaxPixels: maxPx, + private createGhostLayer(id: string, data: T[], getPosition: (d: T) => [number, number], opts: { radiusMinPixels?: number } = {}): ScatterplotLayer { + return new ScatterplotLayer({ + id: `${id}-ghost`, + data, + getPosition, + getRadius: 1, + radiusMinPixels: opts.radiusMinPixels ?? 12, + getFillColor: [0, 0, 0, 0], pickable: true, - stroked: true, - getLineColor: (d) => - d.hasBreaking ? [255, 255, 255, 255] as [number, number, number, number] : [0, 0, 0, 0] as [number, number, number, number], - lineWidthMinPixels: 2, }); } + private createDatacentersLayer(): IconLayer { const highlightedDC = this.highlightedAssets.datacenter; const data = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned'); @@ -1658,8 +1243,6 @@ export class DeckGLMap { }); } - // Note: Protests layer now rendered via HTML overlays in renderProtestClusters() - private createMilitaryVesselsLayer(): ScatterplotLayer { return new ScatterplotLayer({ id: 'military-vessels-layer', @@ -1802,8 +1385,6 @@ export class DeckGLMap { }); } - // Note: Tech HQs layer now rendered via HTML overlays in renderTechHQClusters() - private createAcceleratorsLayer(): ScatterplotLayer { return new ScatterplotLayer({ id: 'accelerators-layer', @@ -1830,17 +1411,299 @@ export class DeckGLMap { }); } + private createProtestClusterLayers(): Layer[] { + this.updateClusterData(); + const layers: Layer[] = []; + + layers.push(new ScatterplotLayer({ + id: 'protest-clusters-layer', + data: this.protestClusters, + getPosition: d => [d.lon, d.lat], + getRadius: d => 15000 + d.count * 2000, + radiusMinPixels: 6, + radiusMaxPixels: 22, + getFillColor: d => { + if (d.hasRiot) return [220, 40, 40, 200] as [number, number, number, number]; + if (d.maxSeverity === 'high') return [255, 80, 60, 180] as [number, number, number, number]; + if (d.maxSeverity === 'medium') return [255, 160, 40, 160] as [number, number, number, number]; + return [255, 220, 80, 140] as [number, number, number, number]; + }, + pickable: true, + updateTriggers: { getRadius: this.lastSCZoom, getFillColor: this.lastSCZoom }, + })); + + layers.push(this.createGhostLayer('protest-clusters-layer', this.protestClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 })); + + const multiClusters = this.protestClusters.filter(c => c.count > 1); + if (multiClusters.length > 0) { + layers.push(new TextLayer({ + id: 'protest-clusters-badge', + data: multiClusters, + getText: d => String(d.count), + getPosition: d => [d.lon, d.lat], + background: true, + getBackgroundColor: [0, 0, 0, 180], + backgroundPadding: [4, 2, 4, 2], + getColor: [255, 255, 255, 255], + getSize: 12, + getPixelOffset: [0, -14], + pickable: false, + fontFamily: 'system-ui, sans-serif', + fontWeight: 700, + })); + } + + const pulseClusters = this.protestClusters.filter(c => c.maxSeverity === 'high' || c.hasRiot); + if (pulseClusters.length > 0) { + const pulse = 1.0 + 0.8 * (0.5 + 0.5 * Math.sin((this.pulseTime || Date.now()) / 400)); + layers.push(new ScatterplotLayer({ + id: 'protest-clusters-pulse', + data: pulseClusters, + getPosition: d => [d.lon, d.lat], + getRadius: d => 15000 + d.count * 2000, + radiusScale: pulse, + radiusMinPixels: 8, + radiusMaxPixels: 30, + stroked: true, + filled: false, + getLineColor: d => d.hasRiot ? [220, 40, 40, 120] as [number, number, number, number] : [255, 80, 60, 100] as [number, number, number, number], + lineWidthMinPixels: 1.5, + pickable: false, + updateTriggers: { radiusScale: this.pulseTime }, + })); + } + + return layers; + } + + private createTechHQClusterLayers(): Layer[] { + this.updateClusterData(); + const layers: Layer[] = []; + const zoom = this.maplibreMap?.getZoom() || 2; + + layers.push(new ScatterplotLayer({ + id: 'tech-hq-clusters-layer', + data: this.techHQClusters, + getPosition: d => [d.lon, d.lat], + getRadius: d => 10000 + d.count * 1500, + radiusMinPixels: 5, + radiusMaxPixels: 18, + getFillColor: d => { + if (d.primaryType === 'faang') return [0, 220, 120, 200] as [number, number, number, number]; + if (d.primaryType === 'unicorn') return [255, 100, 200, 180] as [number, number, number, number]; + return [80, 160, 255, 180] as [number, number, number, number]; + }, + pickable: true, + updateTriggers: { getRadius: this.lastSCZoom }, + })); + + layers.push(this.createGhostLayer('tech-hq-clusters-layer', this.techHQClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 })); + + const multiClusters = this.techHQClusters.filter(c => c.count > 1); + if (multiClusters.length > 0) { + layers.push(new TextLayer({ + id: 'tech-hq-clusters-badge', + data: multiClusters, + getText: d => String(d.count), + getPosition: d => [d.lon, d.lat], + background: true, + getBackgroundColor: [0, 0, 0, 180], + backgroundPadding: [4, 2, 4, 2], + getColor: [255, 255, 255, 255], + getSize: 12, + getPixelOffset: [0, -14], + pickable: false, + fontFamily: 'system-ui, sans-serif', + fontWeight: 700, + })); + } + + if (zoom >= 3) { + const singles = this.techHQClusters.filter(c => c.count === 1); + if (singles.length > 0) { + layers.push(new TextLayer({ + id: 'tech-hq-clusters-label', + data: singles, + getText: d => d.items[0]?.company ?? '', + getPosition: d => [d.lon, d.lat], + getSize: 11, + getColor: [220, 220, 220, 200], + getPixelOffset: [0, 12], + pickable: false, + fontFamily: 'system-ui, sans-serif', + })); + } + } + + return layers; + } + + private createTechEventClusterLayers(): Layer[] { + this.updateClusterData(); + const layers: Layer[] = []; + + layers.push(new ScatterplotLayer({ + id: 'tech-event-clusters-layer', + data: this.techEventClusters, + getPosition: d => [d.lon, d.lat], + getRadius: d => 10000 + d.count * 1500, + radiusMinPixels: 5, + radiusMaxPixels: 18, + getFillColor: d => { + if (d.soonestDaysUntil <= 14) return [255, 220, 50, 200] as [number, number, number, number]; + return [80, 140, 255, 180] as [number, number, number, number]; + }, + pickable: true, + updateTriggers: { getRadius: this.lastSCZoom }, + })); + + layers.push(this.createGhostLayer('tech-event-clusters-layer', this.techEventClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 })); + + const multiClusters = this.techEventClusters.filter(c => c.count > 1); + if (multiClusters.length > 0) { + layers.push(new TextLayer({ + id: 'tech-event-clusters-badge', + data: multiClusters, + getText: d => String(d.count), + getPosition: d => [d.lon, d.lat], + background: true, + getBackgroundColor: [0, 0, 0, 180], + backgroundPadding: [4, 2, 4, 2], + getColor: [255, 255, 255, 255], + getSize: 12, + getPixelOffset: [0, -14], + pickable: false, + fontFamily: 'system-ui, sans-serif', + fontWeight: 700, + })); + } + + return layers; + } + + private createDatacenterClusterLayers(): Layer[] { + this.updateClusterData(); + const layers: Layer[] = []; + + layers.push(new ScatterplotLayer({ + id: 'datacenter-clusters-layer', + data: this.datacenterClusters, + getPosition: d => [d.lon, d.lat], + getRadius: d => 15000 + d.count * 2000, + radiusMinPixels: 6, + radiusMaxPixels: 20, + getFillColor: d => { + if (d.majorityExisting) return [160, 80, 255, 180] as [number, number, number, number]; + return [80, 160, 255, 180] as [number, number, number, number]; + }, + pickable: true, + updateTriggers: { getRadius: this.lastSCZoom }, + })); + + layers.push(this.createGhostLayer('datacenter-clusters-layer', this.datacenterClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 })); + + const multiClusters = this.datacenterClusters.filter(c => c.count > 1); + if (multiClusters.length > 0) { + layers.push(new TextLayer({ + id: 'datacenter-clusters-badge', + data: multiClusters, + getText: d => String(d.count), + getPosition: d => [d.lon, d.lat], + background: true, + getBackgroundColor: [0, 0, 0, 180], + backgroundPadding: [4, 2, 4, 2], + getColor: [255, 255, 255, 255], + getSize: 12, + getPixelOffset: [0, -14], + pickable: false, + fontFamily: 'system-ui, sans-serif', + fontWeight: 700, + })); + } + + return layers; + } + + private createHotspotsLayers(): Layer[] { + const zoom = this.maplibreMap?.getZoom() || 2; + const zoomScale = Math.min(1, (zoom - 1) / 3); + const maxPx = 6 + Math.round(14 * zoomScale); + const baseOpacity = zoom < 2.5 ? 0.5 : zoom < 4 ? 0.7 : 1.0; + const layers: Layer[] = []; + + layers.push(new ScatterplotLayer({ + id: 'hotspots-layer', + data: this.hotspots, + getPosition: (d) => [d.lon, d.lat], + getRadius: (d) => { + const score = d.escalationScore || 1; + return 10000 + score * 5000; + }, + getFillColor: (d) => { + const score = d.escalationScore || 1; + const a = Math.round((score >= 4 ? 200 : score >= 2 ? 200 : 180) * baseOpacity); + if (score >= 4) return [255, 68, 68, a] as [number, number, number, number]; + if (score >= 2) return [255, 165, 0, a] as [number, number, number, number]; + return [255, 255, 0, a] as [number, number, number, number]; + }, + radiusMinPixels: 4, + radiusMaxPixels: maxPx, + pickable: true, + stroked: true, + getLineColor: (d) => + d.hasBreaking ? [255, 255, 255, 255] as [number, number, number, number] : [0, 0, 0, 0] as [number, number, number, number], + lineWidthMinPixels: 2, + })); + + layers.push(this.createGhostLayer('hotspots-layer', this.hotspots, d => [d.lon, d.lat], { radiusMinPixels: 14 })); + + const highHotspots = this.hotspots.filter(h => h.level === 'high' || h.hasBreaking); + if (highHotspots.length > 0) { + const pulse = 1.0 + 0.8 * (0.5 + 0.5 * Math.sin((this.pulseTime || Date.now()) / 400)); + layers.push(new ScatterplotLayer({ + id: 'hotspots-pulse', + data: highHotspots, + getPosition: (d) => [d.lon, d.lat], + getRadius: (d) => { + const score = d.escalationScore || 1; + return 10000 + score * 5000; + }, + radiusScale: pulse, + radiusMinPixels: 6, + radiusMaxPixels: 30, + stroked: true, + filled: false, + getLineColor: (d) => { + const a = Math.round(120 * baseOpacity); + return d.hasBreaking ? [255, 50, 50, a] as [number, number, number, number] : [255, 165, 0, a] as [number, number, number, number]; + }, + lineWidthMinPixels: 1.5, + pickable: false, + updateTriggers: { radiusScale: this.pulseTime }, + })); + + } + + return layers; + } + private pulseTime = 0; - private startNewsPulseAnimation(): void { + private needsPulseAnimation(): boolean { + return this.hasRecentNews(Date.now()) + || this.protestClusters.some(c => c.maxSeverity === 'high' || c.hasRiot) + || this.hotspots.some(h => h.level === 'high' || h.hasBreaking); + } + + private startPulseAnimation(): void { if (this.newsPulseIntervalId !== null) return; const PULSE_UPDATE_INTERVAL_MS = 250; this.newsPulseIntervalId = setInterval(() => { const now = Date.now(); - if (!this.hasRecentNews(now)) { + if (!this.needsPulseAnimation()) { this.pulseTime = now; - this.stopNewsPulseAnimation(); + this.stopPulseAnimation(); this.rafUpdateLayers(); return; } @@ -1849,7 +1712,7 @@ export class DeckGLMap { }, PULSE_UPDATE_INTERVAL_MS); } - private stopNewsPulseAnimation(): void { + private stopPulseAnimation(): void { if (this.newsPulseIntervalId !== null) { clearInterval(this.newsPulseIntervalId); this.newsPulseIntervalId = null; @@ -1932,7 +1795,8 @@ export class DeckGLMap { private getTooltip(info: PickingInfo): { html: string } | null { if (!info.object) return null; - const layerId = info.layer?.id || ''; + const rawLayerId = info.layer?.id || ''; + const layerId = rawLayerId.endsWith('-ghost') ? rawLayerId.slice(0, -6) : rawLayerId; // eslint-disable-next-line @typescript-eslint/no-explicit-any const obj = info.object as any; const text = (value: unknown): string => escapeHtml(String(value ?? '')); @@ -1952,6 +1816,30 @@ export class DeckGLMap { return { html: `
${text(obj.name || 'Flight Cluster')}
${obj.flightCount || 0} aircraft
${text(obj.activityType)}
` }; case 'protests-layer': return { html: `
${text(obj.title)}
${text(obj.country)}
` }; + case 'protest-clusters-layer': + if (obj.count === 1) { + const item = obj.items?.[0]; + return { html: `
${text(item?.title || 'Protest')}
${text(item?.city || item?.country || '')}
` }; + } + return { html: `
${obj.count} protests
${text(obj.country)}
` }; + case 'tech-hq-clusters-layer': + if (obj.count === 1) { + const hq = obj.items?.[0]; + return { html: `
${text(hq?.company || '')}
${text(hq?.city || '')}
` }; + } + return { html: `
${obj.count} tech HQs
${text(obj.city)}
` }; + case 'tech-event-clusters-layer': + if (obj.count === 1) { + const ev = obj.items?.[0]; + return { html: `
${text(ev?.title || '')}
${text(ev?.location || '')}
` }; + } + return { html: `
${obj.count} tech events
${text(obj.location)}
` }; + case 'datacenter-clusters-layer': + if (obj.count === 1) { + const dc = obj.items?.[0]; + return { html: `
${text(dc?.name || '')}
${text(dc?.owner || '')}
` }; + } + return { html: `
${obj.count} data centers
${text(obj.country)}
` }; case 'bases-layer': return { html: `
${text(obj.name)}
${text(obj.country)}
` }; case 'nuclear-layer': @@ -2030,7 +1918,8 @@ export class DeckGLMap { return; } - const layerId = info.layer?.id || ''; + const rawClickLayerId = info.layer?.id || ''; + const layerId = rawClickLayerId.endsWith('-ghost') ? rawClickLayerId.slice(0, -6) : rawClickLayerId; // Hotspots show popup with related news if (layerId === 'hotspots-layer') { @@ -2048,6 +1937,44 @@ export class DeckGLMap { return; } + // Handle cluster layers with single/multi logic + if (layerId === 'protest-clusters-layer') { + const cluster = info.object as MapProtestCluster; + if (cluster.count === 1 && cluster.items[0]) { + this.popup.show({ type: 'protest', data: cluster.items[0], x: info.x, y: info.y }); + } else { + this.popup.show({ type: 'protestCluster', data: { items: cluster.items, country: cluster.country }, x: info.x, y: info.y }); + } + return; + } + if (layerId === 'tech-hq-clusters-layer') { + const cluster = info.object as MapTechHQCluster; + if (cluster.count === 1 && cluster.items[0]) { + this.popup.show({ type: 'techHQ', data: cluster.items[0], x: info.x, y: info.y }); + } else { + this.popup.show({ type: 'techHQCluster', data: { items: cluster.items, city: cluster.city, country: cluster.country }, x: info.x, y: info.y }); + } + return; + } + if (layerId === 'tech-event-clusters-layer') { + const cluster = info.object as MapTechEventCluster; + if (cluster.count === 1 && cluster.items[0]) { + this.popup.show({ type: 'techEvent', data: cluster.items[0], x: info.x, y: info.y }); + } else { + this.popup.show({ type: 'techEventCluster', data: { items: cluster.items, location: cluster.location, country: cluster.country }, x: info.x, y: info.y }); + } + return; + } + if (layerId === 'datacenter-clusters-layer') { + const cluster = info.object as MapDatacenterCluster; + if (cluster.count === 1 && cluster.items[0]) { + this.popup.show({ type: 'datacenter', data: cluster.items[0], x: info.x, y: info.y }); + } else { + this.popup.show({ type: 'datacenterCluster', data: { items: cluster.items, region: cluster.country, country: cluster.country }, x: info.x, y: info.y }); + } + return; + } + // Map layer IDs to popup types const layerToPopupType: Record = { 'conflict-zones-layer': 'conflict', @@ -2269,6 +2196,16 @@ export class DeckGLMap { // Collapse toggle const collapseBtn = toggles.querySelector('.toggle-collapse'); const toggleList = toggles.querySelector('.toggle-list'); + + // Manual scroll: intercept wheel, prevent map zoom, scroll the list ourselves + if (toggleList) { + toggles.addEventListener('wheel', (e) => { + e.stopPropagation(); + e.preventDefault(); + toggleList.scrollTop += e.deltaY; + }, { passive: false }); + toggles.addEventListener('touchmove', (e) => e.stopPropagation(), { passive: false }); + } collapseBtn?.addEventListener('click', () => { toggleList?.classList.toggle('collapsed'); if (collapseBtn) collapseBtn.innerHTML = toggleList?.classList.contains('collapsed') ? '▶' : '▼'; @@ -2476,7 +2413,6 @@ export class DeckGLMap { if (this.deckOverlay) { this.deckOverlay.setProps({ layers: this.buildLayers() }); } - this.renderClusterOverlays(); const elapsed = performance.now() - startTime; if (import.meta.env.DEV && elapsed > 16) { console.warn(`[DeckGLMap] updateLayers took ${elapsed.toFixed(2)}ms (>16ms budget)`); @@ -2659,9 +2595,11 @@ export class DeckGLMap { public setProtests(events: SocialUnrestEvent[]): void { this.protests = events; - this.clusterResultCache.clear(); - this.invalidateClusterElementsByType('protest'); + this.rebuildProtestSupercluster(); this.render(); + if (this.needsPulseAnimation() && this.newsPulseIntervalId === null) { + this.startPulseAnimation(); + } } public setFlightDelays(delays: AirportDelayAlert[]): void { @@ -2693,8 +2631,7 @@ export class DeckGLMap { public setTechEvents(events: TechEventMarker[]): void { this.techEvents = events; - this.clusterResultCache.clear(); - this.invalidateClusterElementsByType('event'); + this.rebuildTechEventSupercluster(); this.render(); } @@ -2728,9 +2665,9 @@ export class DeckGLMap { const hasRecent = this.hasRecentNews(now); if (hasRecent && this.newsPulseIntervalId === null) { - this.startNewsPulseAnimation(); + this.startPulseAnimation(); } else if (!hasRecent) { - this.stopNewsPulseAnimation(); + this.stopPulseAnimation(); } } @@ -2765,8 +2702,10 @@ export class DeckGLMap { updateHotspotEscalation(h.id, matchCount, h.hasBreaking || false, velocity); }); - this.scheduleHotspotOverlaySync({ reconcile: true }); - this.render(); // Debounced + this.render(); + if (this.needsPulseAnimation() && this.newsPulseIntervalId === null) { + this.startPulseAnimation(); + } } /** Get news items related to a hotspot by keyword matching */ @@ -3208,12 +3147,7 @@ export class DeckGLMap { this.moveTimeoutId = null; } - if (this.hotspotSyncRafId !== null) { - cancelAnimationFrame(this.hotspotSyncRafId); - this.hotspotSyncRafId = null; - } - - this.stopNewsPulseAnimation(); + this.stopPulseAnimation(); if (this.resizeObserver) { this.resizeObserver.disconnect(); @@ -3221,9 +3155,6 @@ export class DeckGLMap { } this.layerCache.clear(); - this.clusterElementCache.clear(); - this.lastClusterState.clear(); - this.clusterResultCache.clear(); this.deckOverlay?.finalize(); this.maplibreMap?.remove(); diff --git a/src/components/ETFFlowsPanel.ts b/src/components/ETFFlowsPanel.ts index 5b5c38526..ce1a9ee0a 100644 --- a/src/components/ETFFlowsPanel.ts +++ b/src/components/ETFFlowsPanel.ts @@ -55,7 +55,7 @@ export class ETFFlowsPanel extends Panel { constructor() { super({ id: 'etf-flows', title: 'BTC ETF Tracker', showCount: false }); void this.fetchData(); - this.refreshInterval = setInterval(() => this.fetchData(), 60000); + this.refreshInterval = setInterval(() => this.fetchData(), 3 * 60000); } public destroy(): void { diff --git a/src/components/LiveNewsPanel.ts b/src/components/LiveNewsPanel.ts index 722c89c4d..ffdc80223 100644 --- a/src/components/LiveNewsPanel.ts +++ b/src/components/LiveNewsPanel.ts @@ -284,8 +284,7 @@ export class LiveNewsPanel extends Panel { } private async resolveChannelVideo(channel: LiveChannel, forceFallback = false): Promise { - const preferStableDesktopFallback = this.useDesktopEmbedProxy && !!channel.fallbackVideoId; - const useFallbackVideo = channel.useFallbackOnly || forceFallback || preferStableDesktopFallback; + const useFallbackVideo = channel.useFallbackOnly || forceFallback; const liveVideoId = useFallbackVideo ? null : await fetchLiveVideoId(channel.handle); channel.videoId = liveVideoId || channel.fallbackVideoId; channel.isLive = !!liveVideoId; diff --git a/src/components/MacroSignalsPanel.ts b/src/components/MacroSignalsPanel.ts index e321bd2a6..56da27353 100644 --- a/src/components/MacroSignalsPanel.ts +++ b/src/components/MacroSignalsPanel.ts @@ -70,7 +70,7 @@ export class MacroSignalsPanel extends Panel { constructor() { super({ id: 'macro-signals', title: 'Market Radar', showCount: false }); void this.fetchData(); - this.refreshInterval = setInterval(() => this.fetchData(), 60000); + this.refreshInterval = setInterval(() => this.fetchData(), 3 * 60000); } public destroy(): void { diff --git a/src/components/Map.ts b/src/components/Map.ts index 7a12f29f2..0fd727d09 100644 --- a/src/components/Map.ts +++ b/src/components/Map.ts @@ -1113,8 +1113,9 @@ export class MapComponent { if (assigned.has(i)) continue; const item = items[i]!; + if (!Number.isFinite(item.lat) || !Number.isFinite(item.lon)) continue; const pos = projection([item.lon, item.lat]); - if (!pos) continue; + if (!pos || !Number.isFinite(pos[0]) || !Number.isFinite(pos[1])) continue; const cluster: T[] = [item]; assigned.add(i); @@ -1128,8 +1129,9 @@ export class MapComponent { // Skip if different group keys (e.g., different cities) if (getGroupKey && getGroupKey(other) !== itemKey) continue; + if (!Number.isFinite(other.lat) || !Number.isFinite(other.lon)) continue; const otherPos = projection([other.lon, other.lat]); - if (!otherPos) continue; + if (!otherPos || !Number.isFinite(otherPos[0]) || !Number.isFinite(otherPos[1])) continue; const dx = pos[0] - otherPos[0]; const dy = pos[1] - otherPos[1]; @@ -1150,11 +1152,13 @@ export class MapComponent { const centerLat = sumLat / cluster.length; const centerLon = sumLon / cluster.length; const centerPos = projection([centerLon, centerLat]); + const finalPos = (centerPos && Number.isFinite(centerPos[0]) && Number.isFinite(centerPos[1])) + ? centerPos : pos; clusters.push({ items: cluster, center: [centerLon, centerLat], - pos: centerPos || pos, + pos: finalPos, }); } @@ -1274,12 +1278,7 @@ export class MapComponent { div.style.left = `${pos[0]}px`; div.style.top = `${pos[1]}px`; - const breakingBadge = spot.hasBreaking - ? '
BREAKING
' - : ''; - div.innerHTML = ` - ${breakingBadge}
`; diff --git a/src/components/RuntimeConfigPanel.ts b/src/components/RuntimeConfigPanel.ts index 4ca53b7a7..f291c8bb5 100644 --- a/src/components/RuntimeConfigPanel.ts +++ b/src/components/RuntimeConfigPanel.ts @@ -15,21 +15,49 @@ import { invokeTauri } from '@/services/tauri-bridge'; import { escapeHtml } from '@/utils/sanitize'; import { isDesktopRuntime } from '@/services/runtime'; +const SIGNUP_URLS: Partial> = { + GROQ_API_KEY: 'https://console.groq.com/keys', + OPENROUTER_API_KEY: 'https://openrouter.ai/settings/keys', + FRED_API_KEY: 'https://fred.stlouisfed.org/docs/api/api_key.html', + EIA_API_KEY: 'https://www.eia.gov/opendata/register.php', + CLOUDFLARE_API_TOKEN: 'https://dash.cloudflare.com/profile/api-tokens', + ACLED_ACCESS_TOKEN: 'https://developer.acleddata.com/', + WINGBITS_API_KEY: 'https://wingbits.com/register', + AISSTREAM_API_KEY: 'https://aisstream.io/authenticate', + OPENSKY_CLIENT_ID: 'https://opensky-network.org/login?view=registration', + OPENSKY_CLIENT_SECRET: 'https://opensky-network.org/login?view=registration', +}; + interface RuntimeConfigPanelOptions { mode?: 'full' | 'alert'; + buffered?: boolean; } export class RuntimeConfigPanel extends Panel { private unsubscribe: (() => void) | null = null; private readonly mode: 'full' | 'alert'; + private readonly buffered: boolean; + private pendingSecrets = new Map(); constructor(options: RuntimeConfigPanelOptions = {}) { super({ id: 'runtime-config', title: 'Desktop Configuration', showCount: false }); this.mode = options.mode ?? (isDesktopRuntime() ? 'alert' : 'full'); + this.buffered = options.buffered ?? false; this.unsubscribe = subscribeRuntimeConfig(() => this.render()); this.render(); } + public async commitPendingSecrets(): Promise { + for (const [key, value] of this.pendingSecrets) { + await setSecretValue(key, value); + } + this.pendingSecrets.clear(); + } + + public hasPendingChanges(): boolean { + return this.pendingSecrets.size > 0; + } + public destroy(): void { this.unsubscribe?.(); this.unsubscribe = null; @@ -43,29 +71,17 @@ export class RuntimeConfigPanel extends Panel { const totalFeatures = RUNTIME_FEATURES.length; const availableFeatures = RUNTIME_FEATURES.filter((feature) => isFeatureAvailable(feature.id)).length; const missingFeatures = Math.max(0, totalFeatures - availableFeatures); - const missingSecrets = Array.from( - new Set( - RUNTIME_FEATURES - .flatMap((feature) => feature.requiredSecrets) - .filter((key) => !getSecretState(key).valid), - ), - ); - - const alertTitle = missingFeatures > 0 ? 'Settings not configured' : 'Desktop settings configured'; + const configuredCount = Object.keys(snapshot.secrets).length; + const alertTitle = configuredCount > 0 + ? (missingFeatures > 0 ? 'Some features need API keys' : 'Desktop settings configured') + : 'Configure API keys to unlock features'; const alertClass = missingFeatures > 0 ? 'warn' : 'ok'; - const missingPreview = missingSecrets.length > 0 - ? missingSecrets.slice(0, 4).join(', ') - : 'None'; - const missingTail = missingSecrets.length > 4 ? ` +${missingSecrets.length - 4} more` : ''; this.content.innerHTML = `

${alertTitle}

- ${availableFeatures}/${totalFeatures} features available · ${Object.keys(snapshot.secrets).length} local secrets configured. -

-

- Missing keys: ${escapeHtml(`${missingPreview}${missingTail}`)} + ${availableFeatures}/${totalFeatures} features available${configuredCount > 0 ? ` · ${configuredCount} secrets configured` : ''}.