feat(openclaw): add shopping, supermarket deals, geizhals, and ebay MCP tools

This commit is contained in:
Moritz Graf
2026-09-10 21:32:22 +02:00
parent 2f2f597a67
commit 1e81cdbead
4754 changed files with 1298747 additions and 896 deletions
+449
View File
@@ -0,0 +1,449 @@
<template>
<div class="app-container">
<!-- Optional Retro CRT Scanlines Overlay -->
<div class="crt-overlay" v-if="crtEnabled"></div>
<main class="container">
<!-- SNES / Mad TV Style City & Station Control Hero -->
<HouseHero
:lastUpdated="currentMetrics?.updated_at"
@refresh="fetchData"
@theme-changed="handleThemeChanged"
@crt-changed="handleCrtChanged"
/>
<!-- Error / System Warning Alert (if any) -->
<div v-if="error" class="retro-window error-banner-window">
<div class="retro-window-header error-header">
<div class="retro-window-title">
<span>⚠️</span>
<span>SYSTEM ALERT // COMMUNICATION INTERRUPTED</span>
</div>
</div>
<div class="retro-window-content error-content">
<span class="error-msg">{{ error }}</span>
<button @click="fetchData" class="retro-btn btn-accent">RETRY LINK</button>
</div>
</div>
<!-- ============================================== -->
<!-- GROUP 1: TEMPERATURE & CLIMATE (Includes Dewpoint) -->
<!-- ============================================== -->
<section class="group-section">
<div class="group-header-bar">
<div class="group-title">
<span>🌡️</span>
<span>TEMPERATURE & THERMAL CLIMATE</span>
</div>
<span class="group-hint">[CLICK FOR FULL-SCREEN GRAPH]</span>
</div>
<div class="metrics-list-stack">
<!-- 1. Current Temperature -->
<WeatherMetricCard
title="Temperature"
icon="🌡️"
:value="currentMetrics?.temperature"
unit="°C"
:subText="currentMetrics?.apparent_temperature ? `Feels ${currentMetrics.apparent_temperature.toFixed(1)}°C` : undefined"
:highlight="true"
@inspect="openModal('temperature', 'Current Temperature', '🌡️', currentMetrics?.temperature, '°C', 'Ambient Dry-Bulb Reading')"
/>
<!-- 2. Dew Point (Calculated Metric) -->
<WeatherMetricCard
title="Dew Point"
icon="🌫️"
:value="effectiveDewPoint"
unit="°C"
:subBadge="dewPointComfort.text"
subText="Atmospheric Saturation"
@inspect="openModal('dew_point', 'Dew Point (Calculated)', '🌫️', effectiveDewPoint, '°C', `Atmospheric Saturation: ${dewPointComfort.text}`)"
/>
<!-- 3. Feels Like / Apparent Temperature -->
<WeatherMetricCard
title="Feels Like"
icon="🌤️"
:value="currentMetrics?.apparent_temperature"
unit="°C"
subText="Thermal Comfort Index"
@inspect="openModal('apparent_temperature', 'Apparent Temperature (Feels Like)', '🌤️', currentMetrics?.apparent_temperature, '°C', 'Calculated Thermal Comfort Index')"
/>
<!-- 4. Relative Humidity -->
<WeatherMetricCard
title="Rel. Humidity"
icon="💧"
:value="currentMetrics?.humidity"
unit="%"
:subText="currentMetrics?.humidity ? `${currentMetrics.humidity > 65 ? 'High Moisture' : 'Normal Range'}` : undefined"
@inspect="openModal('humidity', 'Relative Humidity', '💧', currentMetrics?.humidity, '%', 'Air Moisture Saturation Level')"
/>
</div>
</section>
<!-- ============================================== -->
<!-- GROUP 2: ATMOSPHERE & PRECIPITATION -->
<!-- ============================================== -->
<section class="group-section">
<div class="group-header-bar">
<div class="group-title">
<span>🌧️</span>
<span>ATMOSPHERE & PRECIPITATION</span>
</div>
<span class="group-hint">[CLICK FOR FULL-SCREEN GRAPH]</span>
</div>
<div class="metrics-list-stack">
<!-- Pressure -->
<WeatherMetricCard
title="Pressure"
icon="🧭"
:value="currentMetrics?.pressure"
unit="hPa"
:subText="currentMetrics?.pressure ? (currentMetrics.pressure >= 1013 ? 'High Pressure (Fair)' : 'Low Pressure System') : undefined"
@inspect="openModal('pressure', 'Atmospheric Pressure', '🧭', currentMetrics?.pressure, 'hPa', 'Barometric Surface Pressure')"
/>
<!-- Rain Status -->
<WeatherMetricCard
title="Rainfall Condition"
icon="🌧️"
:value="currentMetrics?.rain_status_label || (currentMetrics?.rain_status === 1 ? 'Wet (Rain)' : 'Dry')"
unit=""
:subBadge="currentMetrics?.rain_status === 1 ? 'PRECIPITATION' : 'NO RAIN'"
:subBadgeColor="currentMetrics?.rain_status === 1 ? '#38bdf8' : '#10b981'"
subText="Optical Rain Sensor State"
@inspect="openModal('pressure', 'Precipitation & Barometric Log', '🌧️', currentMetrics?.rain_status_label, '', 'Live Precipitation & Barometric Log')"
/>
</div>
</section>
<!-- ============================================== -->
<!-- GROUP 3: WIND & GUSTS -->
<!-- ============================================== -->
<section class="group-section">
<div class="group-header-bar">
<div class="group-title">
<span>💨</span>
<span>WIND VELOCITY & GUST DYNAMICS</span>
</div>
<span class="group-hint">[CLICK FOR FULL-SCREEN GRAPH]</span>
</div>
<div class="metrics-list-stack">
<!-- Wind Speed -->
<WeatherMetricCard
title="Wind Speed"
icon="💨"
:value="currentMetrics?.wind_speed"
unit="km/h"
:subText="`Direction: ${windCardinal}`"
@inspect="openModal('wind_speed', 'Wind Speed & Flow', '💨', currentMetrics?.wind_speed, 'km/h', `Sustained Velocity • Heading ${windCardinal}`)"
/>
<!-- Gust Speed -->
<WeatherMetricCard
title="Peak Gusts"
icon="🌪️"
:value="currentMetrics?.gust_speed"
unit="km/h"
:subText="currentMetrics?.gust_speed ? `Peak: ${currentMetrics.gust_speed.toFixed(1)} km/h` : undefined"
@inspect="openModal('gust_speed', 'Gust Velocity Dynamics', '🌪️', currentMetrics?.gust_speed, 'km/h', 'Maximum Gust Speed in Sample Window')"
/>
<!-- Wind Direction -->
<WeatherMetricCard
title="Wind Bearing"
icon="🧭"
:value="windCardinal"
unit=""
:subText="currentMetrics?.wind_direction !== undefined && currentMetrics?.wind_direction !== null ? `Angle: ${Math.round(currentMetrics.wind_direction)}°` : undefined"
:subBadge="`${Math.round(currentMetrics?.wind_direction ?? 0)}°`"
@inspect="openModal('wind_speed', 'Wind Direction & Dynamics', '🧭', `${windCardinal} (${Math.round(currentMetrics?.wind_direction ?? 0)}°)`, '', 'Compass Rose Bearing & Flow')"
/>
</div>
</section>
<!-- ============================================== -->
<!-- GROUP 4: SOLAR & RADIATION -->
<!-- ============================================== -->
<section class="group-section">
<div class="group-header-bar">
<div class="group-title">
<span>☀️</span>
<span>SOLAR ILLUMINANCE & UV RADIATION</span>
</div>
<span class="group-hint">[CLICK FOR FULL-SCREEN GRAPH]</span>
</div>
<div class="metrics-list-stack">
<!-- Solar Illuminance -->
<WeatherMetricCard
title="Solar Illuminance"
icon="☀️"
:value="currentMetrics?.illuminance_lux"
unit="LUX"
:subText="currentMetrics?.illuminance_lux && currentMetrics.illuminance_lux > 10000 ? 'Direct Sunlight' : 'Ambient Light'"
@inspect="openModal('illuminance_lux', 'Solar Illuminance', '☀️', currentMetrics?.illuminance_lux, 'LUX', 'Photometric Illuminance from Solar Sensor')"
/>
<!-- UV Index -->
<WeatherMetricCard
title="UV Index"
icon="🔆"
:value="currentMetrics?.uv_index ?? 0"
unit="UVI"
:subBadge="uvLevel.label"
:subText="`Exposure: ${uvLevel.label}`"
@inspect="openModal('uv_index', 'Solar UV Index', '🔆', currentMetrics?.uv_index ?? 0, 'UVI', `Ultraviolet Radiation Index (${uvLevel.label})`)"
/>
</div>
</section>
<!-- ============================================== -->
<!-- GROUP 5: STATION TELEMETRY & SYSTEM STATUS -->
<!-- ============================================== -->
<section class="group-section">
<div class="group-header-bar">
<div class="group-title">
<span>📡</span>
<span>STATION TELEMETRY & LINK QUALITY</span>
</div>
<span class="group-hint">[STATION STATUS]</span>
</div>
<div class="metrics-list-stack">
<!-- Battery -->
<WeatherMetricCard
title="Station Battery"
icon="🔋"
:value="currentMetrics?.battery_percent !== null && currentMetrics?.battery_percent !== undefined ? currentMetrics.battery_percent : '100'"
unit="%"
:subBadge="currentMetrics?.battery_percent && currentMetrics.battery_percent > 30 ? 'HEALTHY' : 'GOOD'"
subText="Solar Power Buffer"
@inspect="openModal('battery_percent', 'Station Battery Level', '🔋', currentMetrics?.battery_percent, '%', 'Onboard Power Cell Capacity')"
/>
<!-- Signal Strength / LQI -->
<WeatherMetricCard
title="Signal Quality (LQI)"
icon="📶"
:value="currentMetrics?.linkquality_lqi !== null && currentMetrics?.linkquality_lqi !== undefined ? currentMetrics.linkquality_lqi : '255'"
unit="LQI"
subText="Shelly / Zigbee Link"
@inspect="openModal('linkquality_lqi', 'Link Quality Indicator', '📶', currentMetrics?.linkquality_lqi, 'LQI', 'RF Signal Quality Indicator')"
/>
<!-- Last Updated -->
<WeatherMetricCard
title="Telemetry Status"
icon="⏱️"
value="ACTIVE"
unit=""
:subText="currentMetrics?.updated_at ? `Sync: ${formatTime(currentMetrics.updated_at)}` : 'Live Stream'"
@inspect="openModal('temperature', 'Live Telemetry Link', '⏱️', 'ONLINE', '', 'Prometheus Real-Time Metrics Pipeline')"
/>
</div>
</section>
</main>
<!-- 16-Bit No-Scroll Detail Screen Overlay -->
<MetricDetailModal
v-if="selectedModalMetric"
:metricKey="selectedModalMetric.key"
:title="selectedModalMetric.title"
:icon="selectedModalMetric.icon"
:value="selectedModalMetric.value"
:unit="selectedModalMetric.unit"
:subText="selectedModalMetric.subText"
:history="history"
:theme="currentTheme"
@close="closeModal"
@change-timeframe="fetchHistoryForHours"
/>
<!-- Retro Footer -->
<footer class="app-footer container">
<p>GRABENGASSE 7 • REGENSBURG // 16-BIT METEOROLOGICAL STATION</p>
<p class="footer-sub">SHELLY WS90 INTEGRATION • PROMETHEUS TIME-SERIES ENGINE • V1.0</p>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import HouseHero from './components/HouseHero.vue'
import WeatherMetricCard from './components/WeatherMetricCard.vue'
import MetricDetailModal from './components/MetricDetailModal.vue'
import type { CurrentWeatherMetrics, WeatherHistoryResponse } from './types/weather'
import {
calculateDewPoint,
getDewPointComfort,
getWindDirectionCardinal,
getUVIndexLevel
} from './utils/weatherCalculations'
const currentMetrics = ref<CurrentWeatherMetrics | null>(null)
const history = ref<WeatherHistoryResponse | undefined>(undefined)
const error = ref<string | null>(null)
const currentTheme = ref<'dark' | 'light'>('dark')
const crtEnabled = ref(true)
interface ModalConfig {
key: string
title: string
icon: string
value?: number | string | null
unit?: string
subText?: string
}
const selectedModalMetric = ref<ModalConfig | null>(null)
let pollTimer: number | null = null
// Calculated Dew Point
const effectiveDewPoint = computed(() => {
if (currentMetrics.value?.dew_point !== undefined && currentMetrics.value?.dew_point !== null) {
return currentMetrics.value.dew_point
}
return calculateDewPoint(
currentMetrics.value?.temperature,
currentMetrics.value?.humidity
)
})
// Dew Point Comfort Description
const dewPointComfort = computed(() => {
return getDewPointComfort(effectiveDewPoint.value)
})
// Wind Direction Cardinal
const windCardinal = computed(() => {
return getWindDirectionCardinal(currentMetrics.value?.wind_direction)
})
// UV Level Info
const uvLevel = computed(() => {
return getUVIndexLevel(currentMetrics.value?.uv_index)
})
function formatTime(isoString?: string): string {
if (!isoString) return ''
try {
const date = new Date(isoString)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
} catch {
return isoString
}
}
function openModal(
key: string,
title: string,
icon: string,
value?: number | string | null,
unit?: string,
subText?: string
) {
selectedModalMetric.value = { key, title, icon, value, unit, subText }
}
function closeModal() {
selectedModalMetric.value = null
}
function handleThemeChanged(theme: 'dark' | 'light') {
currentTheme.value = theme
}
function handleCrtChanged(enabled: boolean) {
crtEnabled.value = enabled
}
async function fetchCurrentMetrics() {
try {
const res = await fetch('/api/v1/weather/current')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
currentMetrics.value = data
error.value = null
} catch (err: any) {
console.error('Error fetching current metrics:', err)
error.value = 'Station Telemetry Offline. Retrying link to Prometheus...'
}
}
async function fetchHistoryForHours(hours: number = 24) {
try {
const res = await fetch(`/api/v1/weather/history?hours=${hours}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
history.value = data
} catch (err: any) {
console.error('Error fetching history:', err)
}
}
async function fetchData() {
await Promise.all([fetchCurrentMetrics(), fetchHistoryForHours(24)])
}
onMounted(() => {
const savedTheme = (localStorage.getItem('grabengasse_theme') as 'dark' | 'light') || 'dark'
currentTheme.value = savedTheme
const savedCrt = localStorage.getItem('grabengasse_crt')
if (savedCrt !== null) {
crtEnabled.value = savedCrt === 'true'
}
fetchData()
pollTimer = window.setInterval(fetchCurrentMetrics, 30000)
})
onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer)
})
</script>
<style scoped>
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
max-width: 100vw;
overflow-x: hidden;
box-sizing: border-box;
}
.error-banner-window {
margin-bottom: 1.25rem;
border-color: #881337 #fb7185 #fb7185 #881337;
}
.error-header {
background: linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #7f1d1d 100%);
}
.error-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--retro-accent-red);
}
.error-msg {
font-weight: 700;
}
.footer-sub {
font-size: 0.65rem;
color: var(--retro-text-muted);
}
</style>