feat(overview): Temperprozess rename, order-count badge, amber transit flow, brighter labels

- Rename line "Temper-Linie" → "Temperprozess" (tab title + i18n de/en)
- Per-press order display: chip row → single colored order-count badge (.ocount)
- New 5th flow colour --transit (amber) for "Auf dem Weg"; --way (cyan) reserved
  for cart/verladen; per-press amber arrows drop to the oven; header "Maschinenhalle"
- Brighter --ink-faint labels to match temper_rejects (dark #7f8b9a, light #4b586b)
- Grid width tweaks (col floor 96px, wrap max-width 1300px) + hide matrix scrollbar
- Rebuilt dist

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Erik 2026-07-02 20:42:54 +02:00
parent a24f7d2c1e
commit 84253f7dcc
14 changed files with 166 additions and 81 deletions

1
dist/assets/index-B4JBFZH3.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
dist/index.html vendored
View File

@ -8,9 +8,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Temper-Linie · Live-Übersicht</title> <title>Temperprozess · Live-Übersicht</title>
<script type="module" crossorigin src="/module/temper_overview/assets/index-C3BK_Qev.js"></script> <script type="module" crossorigin src="/module/temper_overview/assets/index-Vt9ocbug.js"></script>
<link rel="stylesheet" crossorigin href="/module/temper_overview/assets/index-BxZ3b12T.css"> <link rel="stylesheet" crossorigin href="/module/temper_overview/assets/index-B4JBFZH3.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -8,7 +8,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Temper-Linie · Live-Übersicht</title> <title>Temperprozess · Live-Übersicht</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -11,7 +11,8 @@ const { t } = useI18n()
<span class="sw" :style="{ background: 'var(--press)' }"></span> <span class="sw" :style="{ background: 'var(--press)' }"></span>
<span><span class="nm">{{ t('flow.presses') }}</span> <span class="sub">{{ t('flow.pressesSub') }}</span></span> <span><span class="nm">{{ t('flow.presses') }}</span> <span class="sub">{{ t('flow.pressesSub') }}</span></span>
</div> </div>
<div class="node"><span class="sw" :style="{ background: 'var(--way)' }"></span><span class="nm">{{ t('flow.onWay') }}</span></div> <div class="node"><span class="sw" :style="{ background: 'var(--way)' }"></span><span class="nm">{{ t('flow.loaded') }}</span></div>
<div class="node"><span class="sw" :style="{ background: 'var(--transit)' }"></span><span class="nm">{{ t('flow.onWay') }}</span></div>
<div class="node"><span class="sw" :style="{ background: 'var(--oven)' }"></span><span class="nm">{{ t('flow.oven') }}</span></div> <div class="node"><span class="sw" :style="{ background: 'var(--oven)' }"></span><span class="nm">{{ t('flow.oven') }}</span></div>
<div class="node"><span class="sw" :style="{ background: 'var(--done)' }"></span><span class="nm">{{ t('flow.finished') }}</span></div> <div class="node"><span class="sw" :style="{ background: 'var(--done)' }"></span><span class="nm">{{ t('flow.finished') }}</span></div>
<span class="orders"> <span class="orders">

View File

@ -70,7 +70,7 @@ const scrapRate = computed(() => {
border-left-color: var(--press); border-left-color: var(--press);
} }
.kpi.way { .kpi.way {
border-left-color: var(--way); border-left-color: var(--transit);
} }
.kpi.oven { .kpi.oven {
border-left-color: var(--oven); border-left-color: var(--oven);
@ -95,7 +95,7 @@ const scrapRate = computed(() => {
color: var(--press); color: var(--press);
} }
.kpi.way .kv { .kpi.way .kv {
color: var(--way); color: var(--transit);
} }
.kpi.oven .kv { .kpi.oven .kv {
color: var(--oven); color: var(--oven);

View File

@ -24,11 +24,15 @@ const statusText = computed(() =>
<div v-else class="press" :class="press.status"> <div v-else class="press" :class="press.status">
<div class="top"> <div class="top">
<span class="pn">{{ press.name }}</span> <span class="pn">{{ press.name }}</span>
<span
v-if="press.orders.length"
class="ocount"
:style="{ background: orderColor(press.orders.length - 1) }"
:title="t('press.orderCount', { n: press.orders.length })"
>{{ press.orders.length }}</span
>
<span class="stled" :style="{ background: ledColor }"></span> <span class="stled" :style="{ background: ledColor }"></span>
</div> </div>
<div class="chips">
<span v-for="(o, i) in press.orders" :key="i" class="chip" :style="{ background: orderColor(o.colorIndex) }"></span>
</div>
<template v-if="press.status === 'run'"> <template v-if="press.status === 'run'">
<div class="good num">{{ de(press.good) }}</div> <div class="good num">{{ de(press.good) }}</div>
<div class="sub">{{ t('press.good') }} · {{ press.cycleTime }}s</div> <div class="sub">{{ t('press.good') }} · {{ press.cycleTime }}s</div>
@ -91,15 +95,19 @@ const statusText = computed(() =>
border-radius: 50%; border-radius: 50%;
margin-left: auto; margin-left: auto;
} }
.chips { .ocount {
display: flex; display: inline-flex;
gap: 3px; align-items: center;
min-height: 12px; justify-content: center;
} min-width: 17px;
.chip { height: 17px;
width: 12px; padding: 0 4px;
height: 12px; border-radius: 4px;
border-radius: 3px; font-family: var(--mono);
font-size: 0.7rem;
font-weight: 800;
line-height: 1;
color: var(--ground);
} }
.good { .good {
font-family: var(--mono); font-family: var(--mono);

View File

@ -12,6 +12,7 @@ const partsOf = (c: CartOnWay) => c.orders.reduce((s, o) => s + o.qty, 0)
</script> </script>
<template> <template>
<div class="lane-wrap">
<div class="lane" :class="{ active }"> <div class="lane" :class="{ active }">
<span class="conn"></span> <span class="conn"></span>
<span class="lc" :class="active ? 'on' : 'off'"> <span class="lc" :class="active ? 'on' : 'off'">
@ -28,18 +29,33 @@ const partsOf = (c: CartOnWay) => c.orders.reduce((s, o) => s + o.qty, 0)
<div class="cfoot">{{ t('lane.ovenIn', { n: cart.etaMin }) }}</div> <div class="cfoot">{{ t('lane.ovenIn', { n: cart.etaMin }) }}</div>
</div> </div>
</div> </div>
<!-- Connector: bridges the on-the-way lane down to the oven. `.drop` grows to
fill the space below the cart list, so every active column's arrow lands on
the same line and touches the oven box (like the ovenFertig connector). -->
<div v-if="active" class="drop" :title="t('flow.onWay')">
<span class="dropline"></span>
<span class="arrow"></span>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.lane-wrap {
display: flex;
flex-direction: column;
}
.lane { .lane {
position: relative; position: relative;
flex: 0 0 auto;
width: 100%;
min-height: 132px; min-height: 132px;
max-height: 236px; max-height: 236px;
overflow-y: auto; overflow-y: auto;
padding-top: 22px; padding-top: 22px;
padding-bottom: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 12px;
scrollbar-width: thin; scrollbar-width: thin;
} }
.conn { .conn {
@ -50,7 +66,7 @@ const partsOf = (c: CartOnWay) => c.orders.reduce((s, o) => s + o.qty, 0)
width: 2px; width: 2px;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 0; z-index: 0;
background: repeating-linear-gradient(180deg, var(--way) 0 3px, transparent 3px 11px); background: repeating-linear-gradient(180deg, var(--transit) 0 3px, transparent 3px 11px);
background-size: 2px 11px; background-size: 2px 11px;
opacity: 0.3; opacity: 0.3;
} }
@ -73,11 +89,42 @@ const partsOf = (c: CartOnWay) => c.orders.reduce((s, o) => s + o.qty, 0)
z-index: 3; z-index: 3;
} }
.lc.on { .lc.on {
color: var(--way); color: var(--transit);
} }
.lc.off { .lc.off {
color: var(--ink-faint); color: var(--ink-faint);
} }
/* Bridge from the cart lane down to the oven — mirrors the oven→Fertig `.link`. */
.drop {
position: relative;
flex: 1 1 auto;
min-height: 26px;
align-self: stretch;
}
.dropline {
position: absolute;
left: 50%;
top: 0;
bottom: 7px;
width: 2px;
transform: translateX(-50%);
background: repeating-linear-gradient(180deg, var(--transit) 0 3px, transparent 3px 11px);
background-size: 2px 11px;
opacity: 0.95;
animation: flowdown 1.05s linear infinite;
box-shadow: 0 0 6px var(--transit);
}
.arrow {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
color: var(--transit);
font-size: 0.8rem;
line-height: 1;
text-shadow: 0 0 6px var(--transit);
animation: blink 1.1s ease-in-out infinite;
}
.cart { .cart {
position: relative; position: relative;
z-index: 2; z-index: 2;

View File

@ -25,6 +25,10 @@ function cartsFor(pressName: string): CartOnWay[] {
<div class="stagehd"> <div class="stagehd">
<span class="sw" :style="{ background: 'var(--press)' }"></span> <span class="sw" :style="{ background: 'var(--press)' }"></span>
{{ t('line.header') }} {{ t('line.header') }}
<span v-if="wayCarts > 0" class="way-badge">
<span class="dot"></span>
{{ t('kpi.onWaySub', { n: wayCarts }) }}
</span>
<span class="c">{{ t('line.active', { running, total }) }}</span> <span class="c">{{ t('line.active', { running, total }) }}</span>
</div> </div>
@ -35,10 +39,6 @@ function cartsFor(pressName: string): CartOnWay[] {
</div> </div>
</div> </div>
<div class="link" :style="{ '--lc': 'var(--way)' }">
<span class="lbl"><b>{{ t('line.toOven') }}</b> {{ t('line.toOvenTail') }} · {{ t('line.wagen', { n: wayCarts }) }}</span>
</div>
<OvenBand :batches="payload.oven" :temp-c="payload.ovenTempC ?? null" :avg-min="payload.ovenAvgMin" /> <OvenBand :batches="payload.oven" :temp-c="payload.ovenTempC ?? null" :avg-min="payload.ovenAvgMin" />
<div class="link" :style="{ '--lc': 'var(--done)' }"> <div class="link" :style="{ '--lc': 'var(--done)' }">
@ -78,18 +78,45 @@ html:not(.dark) .production {
height: 9px; height: 9px;
border-radius: var(--r); border-radius: var(--r);
} }
.way-badge {
display: flex;
align-items: center;
gap: 6px;
color: var(--transit);
font-weight: 700;
letter-spacing: 0.04em;
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--transit);
box-shadow: 0 0 6px var(--transit);
animation: blink 1.2s ease-in-out infinite;
}
.c { .c {
margin-left: auto; margin-left: auto;
color: var(--ink-dim); color: var(--ink-dim);
} }
.matrix { .matrix {
overflow-x: auto; overflow-x: auto;
/* No gap: the per-press amber connectors (PressLane `.drop`) grow to the bottom of
this grid, so their arrows land flush on the oven band's top edge. Hide the
horizontal scrollbar with the gap gone, a stray/near-miss scrollbar otherwise
rendered as a gray line sitting on the oven's top edge. Hiding it also reclaims
the scrollbar's ~15px, which clears the near-miss overflow so the grid just fits. */
margin-bottom: 0;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* legacy Edge */
}
.matrix::-webkit-scrollbar {
display: none; /* Chromium / WebKit (the Grafana embed) */
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(10, minmax(102px, 1fr)); grid-template-columns: repeat(10, minmax(96px, 1fr));
gap: 8px; gap: 8px;
min-width: 1080px; min-width: 1032px;
} }
.link { .link {
position: relative; position: relative;

View File

@ -2,7 +2,7 @@ export default {
app: { app: {
brand: 'PressV', brand: 'PressV',
plant: 'Werk', plant: 'Werk',
line: 'Temper-Linie', line: 'Temperprozess',
subtitle: 'Live-Übersicht', subtitle: 'Live-Übersicht',
live: 'Live', live: 'Live',
demo: 'Demo-Daten', demo: 'Demo-Daten',
@ -10,6 +10,7 @@ export default {
flow: { flow: {
presses: 'Pressen', presses: 'Pressen',
pressesSub: 'inkl. nicht verladen', pressesSub: 'inkl. nicht verladen',
loaded: 'Auf Wagen verladen',
onWay: 'Auf dem Weg', onWay: 'Auf dem Weg',
oven: 'Ofen', oven: 'Ofen',
finished: 'Fertig', finished: 'Fertig',
@ -29,13 +30,10 @@ export default {
scrapRateSub: 'Ausschuss / Gesamt', scrapRateSub: 'Ausschuss / Gesamt',
}, },
line: { line: {
header: 'Pressenhalle → auf dem Weg', header: 'Maschinenhalle',
active: '{running}/{total} aktiv · Wagen hängen unter ihrer Presse', active: '{running}/{total} aktiv · Wagen hängen unter ihrer Presse',
toOven: 'Auf dem Weg',
toOvenTail: 'in den Ofen',
toStorage: 'Aus dem Ofen', toStorage: 'Aus dem Ofen',
toStorageTail: 'Fertig / Lager', toStorageTail: 'Fertig / Lager',
wagen: '{n} Wagen',
}, },
press: { press: {
good: 'Gut', good: 'Gut',
@ -44,6 +42,7 @@ export default {
statusRun: 'Läuft', statusRun: 'Läuft',
statusStop: 'Störung', statusStop: 'Störung',
statusIdle: 'Leerlauf', statusIdle: 'Leerlauf',
orderCount: '{n} aktive Aufträge',
}, },
lane: { lane: {
wagen: '{n} Wagen ↓', wagen: '{n} Wagen ↓',

View File

@ -3,7 +3,7 @@ export default {
app: { app: {
brand: 'PressV', brand: 'PressV',
plant: 'Plant', plant: 'Plant',
line: 'Temper Line', line: 'Temperprozess',
subtitle: 'Live Overview', subtitle: 'Live Overview',
live: 'Live', live: 'Live',
demo: 'Sample data', demo: 'Sample data',
@ -11,6 +11,7 @@ export default {
flow: { flow: {
presses: 'Presses', presses: 'Presses',
pressesSub: 'incl. not yet loaded', pressesSub: 'incl. not yet loaded',
loaded: 'Loaded onto cart',
onWay: 'On the way', onWay: 'On the way',
oven: 'Oven', oven: 'Oven',
finished: 'Finished', finished: 'Finished',
@ -30,13 +31,10 @@ export default {
scrapRateSub: 'Scrap / total', scrapRateSub: 'Scrap / total',
}, },
line: { line: {
header: 'Press hall → on the way', header: 'Machine hall',
active: '{running}/{total} active · carts hang under their press', active: '{running}/{total} active · carts hang under their press',
toOven: 'On the way',
toOvenTail: 'into the oven',
toStorage: 'Out of the oven', toStorage: 'Out of the oven',
toStorageTail: 'finished / storage', toStorageTail: 'finished / storage',
wagen: '{n} carts',
}, },
press: { press: {
good: 'Good', good: 'Good',
@ -45,6 +43,7 @@ export default {
statusRun: 'Running', statusRun: 'Running',
statusStop: 'Fault', statusStop: 'Fault',
statusIdle: 'Idle', statusIdle: 'Idle',
orderCount: '{n} active orders',
}, },
lane: { lane: {
wagen: '{n} carts ↓', wagen: '{n} carts ↓',

View File

@ -16,7 +16,8 @@
Neutral surfaces are the locked prototype's board palette (dark, layered, Neutral surfaces are the locked prototype's board palette (dark, layered,
with a radial glow). The FLOW ACCENTS match PressV's --c-* process colours with a radial glow). The FLOW ACCENTS match PressV's --c-* process colours
verbatim so the temper process reads the same across every SPRO app verbatim so the temper process reads the same across every SPRO app
(press=violet, on-the-way/cart=cyan, oven=orange, hot=red, finished=emerald). (press=violet, cart/verladen=cyan, in-transit=amber, oven=orange, hot=red,
finished=emerald same 5-station palette as the Ausschuss-Station board).
Font families come from @sp-ui-kit/tokens (Inter Variable + mono). Font families come from @sp-ui-kit/tokens (Inter Variable + mono).
========================================================================== */ ========================================================================== */
:root { :root {
@ -29,10 +30,11 @@
--ink: #e8edf2; --ink: #e8edf2;
--ink-dim: #97a3b1; --ink-dim: #97a3b1;
--ink-faint: #5a6573; --ink-faint: #7f8b9a;
--press: #a78bfa; /* violet — Pressen / not yet loaded */ --press: #a78bfa; /* violet — Pressen / not yet loaded */
--way: #22d3ee; /* cyan — Auf dem Weg / cart */ --way: #22d3ee; /* cyan — Auf Wagen verladen / cart */
--transit: #fbbf24; /* amber — Auf dem Weg (unterwegs) */
--oven: #f97316; /* orange — Ofen */ --oven: #f97316; /* orange — Ofen */
--hot: #ef4444; /* red — Ofen, kurz vor raus */ --hot: #ef4444; /* red — Ofen, kurz vor raus */
--done: #34d399; /* emerald — Fertig / Lager */ --done: #34d399; /* emerald — Fertig / Lager */
@ -63,10 +65,11 @@ html:not(.dark) {
--ink: #0f172a; --ink: #0f172a;
--ink-dim: #475569; --ink-dim: #475569;
--ink-faint: #64748b; --ink-faint: #4b586b;
--press: #6d28d9; --press: #6d28d9;
--way: #0e7490; --way: #0e7490;
--transit: #a16207;
--oven: #c2410c; --oven: #c2410c;
--hot: #dc2626; --hot: #dc2626;
--done: #15803d; --done: #15803d;

View File

@ -1,8 +1,9 @@
<!-- <!--
TemperOverviewPage.vue the whole-line temper board (design #4 "Shop-floor TemperOverviewPage.vue the whole-line temper board (design #4 "Shop-floor
Schematic", locked). One connected line: Pressen (cyan ) Ofen (green ) Schematic", locked). One connected line: Pressen (per-press amber arrows,
Fertig. Carts hang under their origin press; the oven & finished bands scroll only under presses with a cart on the way) Ofen (green ) Fertig. Carts
horizontally. The time-range picker scopes ONLY the Fertig figures. hang under their origin press; the oven & finished bands scroll horizontally.
The time-range picker scopes ONLY the Fertig figures.
Data comes from the store (GET /api/overview, polled). With no backend the Data comes from the store (GET /api/overview, polled). With no backend the
store falls back to bundled sample data, so `npm run dev` renders the design. store falls back to bundled sample data, so `npm run dev` renders the design.
@ -48,7 +49,7 @@ onUnmounted(() => store.stopPolling())
<style scoped> <style scoped>
.wrap { .wrap {
max-width: 1240px; max-width: 1300px;
margin: 0 auto; margin: 0 auto;
padding: 20px 22px 80px; padding: 20px 22px 80px;
} }