SPA_TemperProductionOverview/src/components/OverviewHeader.vue
Erik 360b049af9 fix(spa): reconnect spinner on connection loss, no demo data in prod
On a failed fetch the store now keeps the last real payload (stale > fake)
and flags `disconnected` instead of overwriting live data with bundled demo
data. Sample fallback is gated behind import.meta.env.DEV, so `vite build`
tree-shakes it out entirely (verified: no sample strings in dist).

- cold start / no data: centered reconnect spinner that keeps retrying
- data present + drop: stale data stays, header shows amber "reconnecting"
- polling retries faster (4s) while disconnected, recovers automatically
- sample.ts data moved into sampleOverview() so the module fully tree-shakes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:20:20 +02:00

158 lines
3.7 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RangeCode } from '@/types/overview'
defineProps<{ plant: string; range: RangeCode; sample: boolean; disconnected?: boolean }>()
const emit = defineEmits<{ (e: 'update:range', code: RangeCode): void }>()
const { t } = useI18n()
const RANGES: RangeCode[] = ['12h', '1d', '7d', '14d', '30d']
const clock = ref('')
let timer: ReturnType<typeof setInterval> | null = null
function tick() {
clock.value = new Date().toLocaleTimeString('de-DE')
}
onMounted(() => {
tick()
timer = setInterval(tick, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<template>
<div class="bar">
<span class="mark">{{ t('app.brand') }}</span>
<span class="title">{{ plant }} · <b>{{ t('app.line') }}</b> · {{ t('app.subtitle') }}</span>
<span v-if="sample" class="demo">{{ t('app.demo') }}</span>
<span class="spacer"></span>
<span v-if="disconnected" class="reconnect"><span class="spinner sm" aria-hidden="true"></span>{{ t('conn.reconnecting') }}</span>
<span v-else class="live"><span class="led"></span>{{ t('app.live') }}</span>
<span class="clock num">{{ clock }}</span>
<span class="rangep" role="group" :aria-label="t('range.label')">
<span class="rl">{{ t('range.label') }}</span>
<button
v-for="code in RANGES"
:key="code"
type="button"
:aria-pressed="code === range"
@click="emit('update:range', code)"
>
{{ code }}
</button>
</span>
</div>
</template>
<style scoped>
.bar {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
border: 1px solid var(--line);
background: linear-gradient(180deg, var(--panel), color-mix(in srgb, var(--panel) 70%, transparent));
border-radius: var(--r);
flex-wrap: wrap;
}
.mark {
font-family: var(--mono);
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
font-size: 0.75rem;
color: var(--ground);
background: var(--way);
padding: 5px 9px;
border-radius: var(--r);
}
.title {
font-weight: 650;
font-size: 1rem;
}
.title b {
color: var(--way);
}
.demo {
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--oven);
border: 1px solid color-mix(in srgb, var(--oven) 50%, transparent);
border-radius: var(--r);
padding: 2px 7px;
}
.spacer {
margin-left: auto;
}
.live {
display: flex;
align-items: center;
gap: 7px;
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-dim);
}
.led {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--run);
animation: blink 2.4s ease-in-out infinite;
}
.reconnect {
display: flex;
align-items: center;
gap: 7px;
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--transit);
}
.clock {
font-size: 0.95rem;
color: var(--ink-dim);
}
.rangep {
display: inline-flex;
border: 1px solid var(--line-2);
border-radius: var(--r);
overflow: hidden;
}
.rl {
align-self: center;
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
padding: 0 11px;
}
.rangep button {
font-family: var(--mono);
background: transparent;
border: 0;
border-left: 1px solid var(--line);
color: var(--ink-dim);
padding: 8px 13px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
}
.rangep button:hover {
background: var(--panel-3);
color: var(--ink);
}
.rangep button[aria-pressed='true'] {
background: var(--way);
color: var(--ground);
}
</style>