feat: implement PolyWeather dashboard UI with Leaflet map integration and backend data collection support
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,18 @@ function ScanTerminalScreen() {
|
||||
const aiHydrationQueueRef = useRef<string[]>([]);
|
||||
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 (
|
||||
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root)}>
|
||||
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root, themeMode === "light" && "light")}>
|
||||
<div className={clsx("scan-terminal", themeMode === "light" && "light")}>
|
||||
<main className="scan-data-grid">
|
||||
<div className="scan-loading-state">
|
||||
@@ -557,7 +569,7 @@ function ScanTerminalScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root)}>
|
||||
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root, themeMode === "light" && "light")}>
|
||||
<div className={clsx("scan-terminal", themeMode === "light" && "light")}>
|
||||
<main className="scan-data-grid">
|
||||
<div className="scan-topbar">
|
||||
|
||||
@@ -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:
|
||||
'© <a href="https://www.openstreetmap.org/">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
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();
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user