fix(espidf): accurate core-lib warnings + gateway open hook

Two unrelated polish fixes.

espidf_compiler: headers that resolve to an arduino-esp32 CORE lib
(WebServer, WiFi, …) were correctly skipped from the user-lib merge but
then fell through to a scary "Library for <X> not found — build may
fail" warning — even though the build succeeds because the symbols are
compiled into the core. Now logs an accurate "provided by arduino-esp32
core — already compiled in, not merging". Same treatment for core
headers that aren't standalone lib dirs (Udp.h, IPAddress.h,
WiFiUdp.h, …) via a new _CORE_ESP32_HEADERS allowlist.

SimulatorCanvas: the WiFi badge's "open IoT gateway" click now consults
an optional window.__velxio_iot_gateway_open_gate__ hook before opening
the gateway tab. A private overlay can install it to gate the gateway
behind a paid plan and show an in-place upgrade modal instead of dumping
a 402 page in a new tab. OSS builds have no hook → opens normally. The
check is synchronous so it doesn't trip popup blockers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-05-29 20:38:03 +02:00
parent 594291c830
commit 3a2abc48d9
2 changed files with 45 additions and 2 deletions

View File

@ -619,6 +619,17 @@ class ESPIDFCompiler:
'Preferences', 'Update', 'Ticker', 'Preferences', 'Update', 'Ticker',
}) })
# Headers that ship inside the arduino-esp32 core but don't live in a
# standalone library dir (so _find_library_for_header can't resolve
# them). They're already compiled into the core — pulled transitively
# by WiFi/WebServer/etc. — so a "not found" warning for them is a
# false positive. Treated as core-provided, not "may fail".
_CORE_ESP32_HEADERS: frozenset[str] = frozenset({
'Udp.h', 'IPAddress.h', 'Client.h', 'Server.h', 'Stream.h',
'Print.h', 'Printable.h', 'WiFiUdp.h', 'WiFiClient.h',
'WiFiServer.h', 'WiFiType.h', 'esp_wifi.h',
})
def _resolve_library_components( def _resolve_library_components(
self, self,
ext_headers: list[str], ext_headers: list[str],
@ -673,12 +684,22 @@ class ESPIDFCompiler:
else None else None
) )
# Tracks the "resolved to a core lib that's already compiled into
# the arduino-esp32 component" case, so we don't fall through to
# the scary "not found — build may fail" warning below for a
# header that WAS found (just not as a mergeable user lib).
is_core_provided = False
if src_root is None and esp32_libs and esp32_libs.is_dir(): if src_root is None and esp32_libs and esp32_libs.is_dir():
esp32_root = self._find_library_for_header(header, esp32_libs) esp32_root = self._find_library_for_header(header, esp32_libs)
if esp32_root: if esp32_root:
lib_name = esp32_root.parent.name if esp32_root.name == 'src' else esp32_root.name lib_name = esp32_root.parent.name if esp32_root.name == 'src' else esp32_root.name
if lib_name in self._CORE_ESP32_LIBS: if lib_name in self._CORE_ESP32_LIBS:
logger.debug(f'[espidf] <{header}> is bundled core lib "{lib_name}", skipping') is_core_provided = True
logger.info(
f'[espidf] <{header}> provided by arduino-esp32 core '
f'("{lib_name}") — already compiled in, not merging'
)
else: else:
logger.info(f'[espidf] <{header}> found in esp32_libs as "{lib_name}", merging') logger.info(f'[espidf] <{header}> found in esp32_libs as "{lib_name}", merging')
src_root = esp32_root src_root = esp32_root
@ -742,6 +763,15 @@ class ESPIDFCompiler:
headers_to_resolve.append(th) headers_to_resolve.append(th)
except OSError: except OSError:
pass pass
elif is_core_provided or header in self._CORE_ESP32_HEADERS:
# Resolved to an arduino-esp32 core lib, or a known core
# header that lives inside the core (not a standalone lib
# dir). Already compiled in — not a "build may fail" case.
if header in self._CORE_ESP32_HEADERS:
logger.info(
f'[espidf] <{header}> is an arduino-esp32 core header — '
f'already compiled in, not merging'
)
else: else:
logger.warning(f'[espidf] Library for <{header}> not found — build may fail') logger.warning(f'[espidf] Library for <{header}> not found — build may fail')

View File

@ -2154,10 +2154,23 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
'http://localhost:8001/api'; 'http://localhost:8001/api';
const gatewayUrl = `${backendBase}/gateway/${clientId}/`; const gatewayUrl = `${backendBase}/gateway/${clientId}/`;
const openGateway = () => {
if (!hasIp) return;
// A private overlay (velxio.dev) can install a synchronous
// gate to keep the IoT gateway behind a paid plan. When it
// returns true it has already handled the click (e.g. shown
// an in-place upgrade modal), so we don't open the tab.
// OSS builds have no hook → always open.
const gate = (window as unknown as {
__velxio_iot_gateway_open_gate__?: () => boolean;
}).__velxio_iot_gateway_open_gate__;
if (gate && gate()) return;
window.open(gatewayUrl, '_blank');
};
return ( return (
<span <span
className={`canvas-wifi-badge canvas-wifi-${status}${hasIp ? ' canvas-wifi-clickable' : ''}`} className={`canvas-wifi-badge canvas-wifi-${status}${hasIp ? ' canvas-wifi-clickable' : ''}`}
onClick={() => hasIp && window.open(gatewayUrl, '_blank')} onClick={openGateway}
title={ title={
hasIp hasIp
? `WiFi: ${activeBoard.wifiStatus.ssid ?? 'Velxio-GUEST'} — IP: ${activeBoard.wifiStatus.ip}\nClick to open IoT Gateway ↗` ? `WiFi: ${activeBoard.wifiStatus.ssid ?? 'Velxio-GUEST'} — IP: ${activeBoard.wifiStatus.ip}\nClick to open IoT Gateway ↗`