Initial commit with 2026 World Cup Quant Platform core modules and CI/CD

This commit is contained in:
QuantBot
2026-06-13 23:18:18 +08:00
commit 073abf98c1
155 changed files with 19539 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7d2a15" />
<stop offset="100%" stop-color="#f6a45a" />
</linearGradient>
</defs>
<rect width="192" height="192" rx="30" fill="url(#g)" />
<rect x="22" y="22" width="148" height="148" rx="20" fill="#f6f0e1" />
<rect x="40" y="56" width="112" height="80" rx="8" fill="#7d2a15" />
<text x="56" y="112" fill="#fff8e5" font-size="26" font-family="monospace">FIFA</text>
</svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<radialGradient id="g" cx="0.2" cy="0.2" r="1">
<stop offset="0%" stop-color="#7d2a15" />
<stop offset="100%" stop-color="#f6a45a" />
</radialGradient>
</defs>
<rect width="512" height="512" rx="64" fill="url(#g)" />
<rect x="72" y="72" width="368" height="296" rx="30" fill="#f6f0e1" />
<rect x="104" y="116" width="304" height="208" rx="18" fill="#7d2a15" />
<text x="136" y="258" fill="#fff8e5" font-size="64" font-family="monospace">FIFA</text>
<text x="136" y="332" fill="#fff8e5" font-size="48" font-family="monospace">2026</text>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,33 @@
{
"name": "2026 FIFA Quantum Ops",
"short_name": "FIFA 2026 Ops",
"start_url": "/",
"scope": "/",
"display": "standalone",
"lang": "zh-Hant",
"orientation": "portrait",
"background_color": "#f6f0e1",
"theme_color": "#7d2a15",
"description": "台北時區驅動的 2026 世界盃量化投注研究平台",
"icons": [
{
"src": "/manifest-icon-192.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/manifest-icon-512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
}
],
"screenshots": [
{
"src": "/manifest-icon-512.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
}

210
platform/web/public/sw.js Normal file
View File

@@ -0,0 +1,210 @@
const CACHE_NAME = 'wc2026-quant-v1';
const OFFLINE_URL = '/offline';
const ASSETS = ['/', '/offline', '/manifest.json', '/manifest-icon-192.svg', '/manifest-icon-512.svg'];
const ODDS_KEYS = 'wc2026:lastOddsSnapshot';
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)));
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)));
await self.clients.claim();
})(),
);
});
function cacheSafeCopy(request, response) {
const cacheable = request.method === 'GET' &&
(request.url.includes('/api/analytics') || request.url.includes('/_next') || request.url.endsWith('.png') || request.url.endsWith('.svg') || request.url.endsWith('.css') || request.url.endsWith('.js'));
if (!cacheable) {
return;
}
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, response.clone());
});
}
function extractOddsSnapshot(payload) {
if (!payload || !Array.isArray(payload)) {
return [];
}
const now = new Date().toISOString();
return payload
.filter((item) => item?.market_type && item?.selection && typeof item?.decimal_odds === 'number')
.slice(0, 20)
.map((item) => ({
match_id: item.match_id,
home_team: item.home_team,
away_team: item.away_team,
odds: Number(item.decimal_odds),
market_type: item.market_type,
selection: item.selection,
captured_at: now,
}));
}
async function writeOddsSnapshotFromDetail(data) {
if (!data || typeof data !== 'object') {
return;
}
const rows = [];
if (Array.isArray(data.odds_series)) {
for (const row of data.odds_series) {
if (row && row.bookmaker) {
rows.push({
match_id: data.match_id,
home_team: data.home_team,
away_team: data.away_team,
decimal_odds: Number(row.decimal_odds),
market_type: row.market_type,
selection: row.selection,
});
}
}
}
const snapshot = extractOddsSnapshot(rows);
if (snapshot.length === 0) {
return;
}
const cache = await caches.open(CACHE_NAME);
await cache.put(new Request(`/api/${ODDS_KEYS}`),
new Response(JSON.stringify(snapshot), {
headers: { 'Content-Type': 'application/json' },
}));
const clientsList = await self.clients.matchAll({ type: 'window' });
clientsList.forEach((client) => {
client.postMessage({ type: 'odds-cache-updated', data: snapshot });
});
}
self.addEventListener('fetch', (event) => {
const request = event.request;
if (request.method !== 'GET' || request.url.includes('/api/analytics/proof-of-yield')) {
return;
}
const url = new URL(request.url);
if (url.pathname === '/sw.js') {
return;
}
event.respondWith(
(async () => {
const cached = await caches.match(request);
try {
const response = await fetch(request);
if (!response.ok) {
return cached || Promise.reject(new TypeError('response-not-ok'));
}
if (url.pathname.startsWith('/api/analytics/matches')) {
const clone = response.clone();
cacheSafeCopy(request, response);
const payload = await clone.json().catch(() => null);
await writeOddsSnapshotFromDetail(payload).catch(() => {});
} else {
cacheSafeCopy(request, response);
}
return response;
} catch {
if (cached) {
return cached;
}
if (request.mode === 'navigate') {
return caches.match(OFFLINE_URL);
}
return new Response('Service unavailable', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
})(),
);
});
self.addEventListener('message', (event) => {
if (event.data?.type === 'clear-cache') {
caches.delete(CACHE_NAME);
}
if (event.data?.type === 'persist-odds' && event.data?.payload) {
const payload = event.data.payload;
caches.open(CACHE_NAME).then((cache) => {
cache.put(
new Request(`/api/${ODDS_KEYS}`),
new Response(JSON.stringify(payload), {
headers: { 'Content-Type': 'application/json' },
}),
);
});
}
});
self.addEventListener('push', (event) => {
let notify = {
title: '🚨 鎖利警報',
body: '鎖利機會已觸發,請檢查您的串關進度並考慮提前對沖。',
url: '/portfolio',
};
if (event.data) {
try {
const payload = event.data.json();
notify = {
title: payload.title || notify.title,
body: payload.body || notify.body,
url: payload.url || notify.url,
};
} catch {
notify.body = event.data.text() || notify.body;
}
}
event.waitUntil(
self.registration.showNotification(notify.title, {
body: notify.body,
icon: '/manifest-icon-192.svg',
badge: '/manifest-icon-192.svg',
data: { url: notify.url },
tag: 'wc2026-alert',
requireInteraction: true,
vibrate: [80, 80, 80],
}),
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const target = event.notification.data?.url || '/';
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
for (const client of clients) {
if (client.url === `${self.location.origin}${target}` && 'focus' in client) {
return client.focus();
}
}
if (self.clients.openWindow) {
return self.clients.openWindow(target);
}
return undefined;
}),
);
});