feat(openclaw): add shopping, supermarket deals, geizhals, and ebay MCP tools
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user