diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index b0371da5..f2729748 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -14052,3 +14052,123 @@ padding: 0 13px 13px; } } + +/* Light-mode hard overrides for the scan terminal. Keep this block last so the + decision workspace cannot inherit the dark map/card palette from base rules. */ +.root:global(.light) { + --bg-primary: #eef7ff; + --bg-secondary: #e0f2fe; + --bg-card: #ffffff; + --bg-glass: rgba(255, 255, 255, 0.92); + --border-glass: #cfe8ff; + --border-subtle: rgba(125, 171, 214, 0.34); + --text-primary: #0f172a; + --text-secondary: #334155; + --text-muted: #475569; +} + +.root:global(.light) :global(.scan-terminal.light) { + background: + radial-gradient(circle at 18% 12%, rgba(59, 130, 246, 0.12), transparent 30%), + linear-gradient(180deg, #eef7ff 0%, #e8f4ff 48%, #f8fbff 100%) !important; + color: #0f172a !important; +} + +:global(html.light) .root :global(.map), +.root:global(.light) :global(.map), +.root:global(.light) :global(.scan-terminal.light .scan-map-shell .map), +.root:global(.light) :global(.scan-terminal.light .scan-map-shell .leaflet-container), +.root:global(.light) :global(.leaflet-container), +.root:global(.light) :global(.leaflet-pane), +.root:global(.light) :global(.leaflet-map-pane), +.root:global(.light) :global(.leaflet-tile-pane) { + background: #eef7ff !important; +} + +:global(html.light) .root :global(.map), +.root:global(.light) :global(.scan-terminal.light .scan-map-shell) { + border-color: rgba(37, 99, 235, 0.18) !important; + box-shadow: + 0 18px 40px rgba(61, 100, 145, 0.14), + inset 0 0 0 1px rgba(255, 255, 255, 0.72) !important; +} + +:global(html.light) .root :global(.map .leaflet-tile), +.root:global(.light) :global(.map .leaflet-tile), +.root:global(.light) :global(.scan-terminal.light .scan-map-shell .leaflet-tile) { + filter: none !important; + opacity: 1 !important; +} + +.root:global(.light) :global(.scan-city-detail-rail), +.root:global(.light) :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) { + background: + linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(255, 255, 255, 0.96)) !important; + border-color: rgba(37, 99, 235, 0.16) !important; + color: #0f172a !important; +} + +.root:global(.light) :global(.scan-city-detail-rail .panel-header), +.root:global(.light) :global(.scan-city-detail-rail .detail-structured-section), +.root:global(.light) :global(.scan-city-detail-rail .detail-summary-shell), +.root:global(.light) :global(.scan-city-detail-rail .detail-card) { + background: rgba(255, 255, 255, 0.74) !important; + border-color: rgba(125, 171, 214, 0.28) !important; +} + +.root:global(.light) :global(.scan-city-detail-rail h2), +.root:global(.light) :global(.scan-city-detail-rail h3), +.root:global(.light) :global(.scan-city-detail-rail strong), +.root:global(.light) :global(.scan-city-detail-rail .detail-value), +.root:global(.light) :global(.scan-city-detail-rail .detail-summary-temp), +.root:global(.light) :global(.scan-city-detail-rail .detail-section-head h3), +.root:global(.light) :global(.scan-city-detail-rail .panel-title-area h2) { + color: #0f172a !important; +} + +.root:global(.light) :global(.scan-city-detail-rail p), +.root:global(.light) :global(.scan-city-detail-rail li), +.root:global(.light) :global(.scan-city-detail-rail small), +.root:global(.light) :global(.scan-city-detail-rail .panel-overline), +.root:global(.light) :global(.scan-city-detail-rail .panel-loading-hint), +.root:global(.light) :global(.scan-city-detail-rail .panel-meta-chip), +.root:global(.light) :global(.scan-city-detail-rail .detail-section-kicker), +.root:global(.light) :global(.scan-city-detail-rail .detail-label), +.root:global(.light) :global(.scan-city-detail-rail .detail-value-muted), +.root:global(.light) :global(.scan-city-detail-rail .detail-source-note), +.root:global(.light) :global(.scan-city-detail-rail .detail-source-kind), +.root:global(.light) :global(.scan-city-detail-rail .detail-mini-meta), +.root:global(.light) :global(.scan-city-detail-rail .detail-summary-supporting), +.root:global(.light) :global(.scan-city-detail-rail .scan-detail-city-sub), +.root:global(.light) :global(.scan-city-detail-rail .scan-detail-volume-caption), +.root:global(.light) :global(.scan-city-detail-rail .scan-detail-score-label), +.root:global(.light) :global(.scan-city-detail-rail .scan-detail-empty) { + color: #334155 !important; +} + +.root:global(.light) :global(.scan-city-detail-rail .panel-meta-chip), +.root:global(.light) :global(.scan-city-detail-rail .panel-weather-chip), +.root:global(.light) :global(.scan-city-detail-rail .detail-source-link), +.root:global(.light) :global(.scan-city-detail-rail .panel-action-button-secondary), +.root:global(.light) :global(.scan-city-detail-rail .panel-action-button-ghost) { + background: #eef7ff !important; + border-color: rgba(37, 99, 235, 0.18) !important; + color: #1f3654 !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-section), +.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-band), +.root:global(.light) :global(.scan-terminal.light .scan-ai-market-decision), +.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-metrics span), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-pills span) { + background: #eef7ff !important; + border-color: rgba(37, 99, 235, 0.16) !important; +} + +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card p), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card li), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card small), +.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) { + color: #334155 !important; +} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index c851a8e5..6a28068a 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -78,6 +78,18 @@ function ScanTerminalScreen() { const aiHydrationQueueRef = useRef([]); const aiHydrationRunningRef = useRef(false); + useEffect(() => { + const root = document.documentElement; + const hadLight = root.classList.contains("light"); + const hadDark = root.classList.contains("dark"); + root.classList.toggle("light", themeMode === "light"); + root.classList.toggle("dark", themeMode === "dark"); + return () => { + root.classList.toggle("light", hadLight); + root.classList.toggle("dark", hadDark); + }; + }, [themeMode]); + const timeSortedRows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), [terminalData?.rows], @@ -537,7 +549,7 @@ function ScanTerminalScreen() { if (store.proAccess.loading) { return ( -
+
@@ -557,7 +569,7 @@ function ScanTerminalScreen() { } return ( -
+
diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index ebbedaf3..02b49c30 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -57,14 +57,16 @@ function getMarkerDisplayOffset(cityName: string) { }; } -function getMapTileUrl() { +function getMapTileUrl(container?: Element | null) { if (typeof document === "undefined") { return MAP_TILE_URLS.dark; } const lightMode = + Boolean(container?.closest(".light")) || document.documentElement.classList.contains("light") || document.body.classList.contains("light") || + Boolean(document.querySelector(`.${CSS.escape("light")}`)) || Boolean(document.querySelector(".scan-terminal.light")); if (lightMode) { return MAP_TILE_URLS.light; @@ -518,7 +520,7 @@ export function useLeafletMap({ }); L.control.zoom({ position: "bottomright" }).addTo(map); - const tileLayer = L.tileLayer(getMapTileUrl(), { + const tileLayer = L.tileLayer(getMapTileUrl(container), { attribution: '© OSM © CARTO', maxZoom: 19, @@ -599,7 +601,7 @@ export function useLeafletMap({ if (!tileLayer || typeof MutationObserver === "undefined") return; const syncMapTheme = () => { - tileLayer.setUrl(getMapTileUrl()); + tileLayer.setUrl(getMapTileUrl(containerRef.current)); }; syncMapTheme(); diff --git a/src/data_collection/polymarket_readonly.py b/src/data_collection/polymarket_readonly.py index 681ff89c..513341a0 100644 --- a/src/data_collection/polymarket_readonly.py +++ b/src/data_collection/polymarket_readonly.py @@ -1749,12 +1749,19 @@ class PolymarketReadOnlyLayer: def _fetch_token_market_data(self, token_id: str) -> Dict[str, Any]: # REST-only path: CLOB public endpoints. - bid = _extract_price(self._clob_get("/price", {"token_id": token_id, "side": "BUY"})) - ask = _extract_price( + # Polymarket CLOB semantics: + # - side=BUY returns the executable ask, i.e. the price paid to buy. + # - side=SELL returns the executable bid, i.e. the price received to sell. + buy_price = _extract_price(self._clob_get("/price", {"token_id": token_id, "side": "BUY"})) + sell_price = _extract_price( self._clob_get("/price", {"token_id": token_id, "side": "SELL"}) ) if self.fast_price_only: - buy, sell = self._resolve_trade_prices(buy=ask, sell=bid, book=None) + buy, sell = self._resolve_trade_prices( + buy=buy_price, + sell=sell_price, + book=None, + ) midpoint = (buy + sell) / 2.0 if buy is not None and sell is not None else (buy or sell) spread = max(0.0, float(buy) - float(sell)) if buy is not None and sell is not None else None return { @@ -1775,7 +1782,11 @@ class PolymarketReadOnlyLayer: ) orderbook_raw = self._clob_get("/book", {"token_id": token_id}) book, book_liquidity = self._normalize_orderbook(orderbook_raw) - buy, sell = self._resolve_trade_prices(buy=ask, sell=bid, book=book) + buy, sell = self._resolve_trade_prices( + buy=buy_price, + sell=sell_price, + book=book, + ) if midpoint is None and buy is not None and sell is not None: midpoint = (buy + sell) / 2.0 spread = max(0.0, float(buy) - float(sell)) if buy is not None and sell is not None else None @@ -2366,22 +2377,22 @@ class PolymarketReadOnlyLayer: if not missing: return results - ask_map: Dict[str, float] = {} - bid_map: Dict[str, float] = {} + buy_map: Dict[str, float] = {} + sell_map: Dict[str, float] = {} midpoint_map: Dict[str, float] = {} spread_map: Dict[str, float] = {} last_trade_map: Dict[str, float] = {} book_map: Dict[str, Dict[str, Any]] = {} for chunk in self._batch_chunks(missing): - sell_payload = self._clob_post( - "/prices", - [{"token_id": token_id, "side": "SELL"} for token_id in chunk], - ) buy_payload = self._clob_post( "/prices", [{"token_id": token_id, "side": "BUY"} for token_id in chunk], ) + sell_payload = self._clob_post( + "/prices", + [{"token_id": token_id, "side": "SELL"} for token_id in chunk], + ) midpoint_payload = ( self._clob_post( "/midpoints", @@ -2414,8 +2425,8 @@ class PolymarketReadOnlyLayer: if include_books and not self.fast_price_only else None ) - ask_map.update(self._extract_batch_price_map(sell_payload, "SELL")) - bid_map.update(self._extract_batch_price_map(buy_payload, "BUY")) + buy_map.update(self._extract_batch_price_map(buy_payload, "BUY")) + sell_map.update(self._extract_batch_price_map(sell_payload, "SELL")) midpoint_map.update(self._extract_batch_scalar_map(midpoint_payload)) spread_map.update(self._extract_batch_scalar_map(spread_payload)) last_trade_map.update(self._extract_batch_scalar_map(last_trade_payload)) @@ -2427,13 +2438,17 @@ class PolymarketReadOnlyLayer: for token_id in missing: raw_book = book_map.get(token_id) book, book_liquidity = self._normalize_orderbook(raw_book) - ask = _extract_price(ask_map.get(token_id)) - bid = _extract_price(bid_map.get(token_id)) + buy_price = _extract_price(buy_map.get(token_id)) + sell_price = _extract_price(sell_map.get(token_id)) midpoint = _extract_price(midpoint_map.get(token_id)) spread = _extract_price(spread_map.get(token_id)) last_trade = _extract_price(last_trade_map.get(token_id)) - buy, sell = self._resolve_trade_prices(buy=ask, sell=bid, book=book) + buy, sell = self._resolve_trade_prices( + buy=buy_price, + sell=sell_price, + book=book, + ) if midpoint is None and buy is not None and sell is not None: midpoint = (buy + sell) / 2.0 if midpoint is None: