fix(briefing): 2-column layout with table-based weather forecast

Collapse briefing from 3-column to 2-column grid (chat + sidebar).
Weather moves to top of news column, forecast uses an HTML table so
rows align cleanly. Drop verbose condition text from forecast days
(emoji already conveys it). Switch CI node_modules cache to npm
download cache to avoid tar size error from onnxruntime-web.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 18:10:46 -04:00
parent a4995606e5
commit f12d29563a
3 changed files with 86 additions and 105 deletions
+4 -9
View File
@@ -68,19 +68,14 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
# Cache node_modules directly (not the npm download cache). - name: Cache npm download cache
# npm ci still has to extract + link every module even with a
# warm download cache, which is where the real time goes. Caching
# the output directory lets us skip npm ci entirely on hits.
- name: Cache node_modules
id: npm-cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: frontend/node_modules path: ~/.npm
key: node-modules-${{ hashFiles('frontend/package-lock.json') }} key: npm-cache-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: npm-cache-
- name: Install dependencies - name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci run: npm ci
working-directory: frontend working-directory: frontend
+55 -41
View File
@@ -85,22 +85,30 @@ const fetchedAtLabel = computed(() => {
Today: {{ weather.today_high }}° / {{ weather.today_low }}° Today: {{ weather.today_high }}° / {{ weather.today_low }}°
<span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span> <span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span>
</div> </div>
<div class="weather-forecast" v-if="weather.forecast.length"> <table class="weather-forecast" v-if="weather.forecast.length">
<div v-for="day in weather.forecast" :key="day.day" class="weather-forecast-day"> <thead>
<span class="forecast-day-name">{{ day.day }}</span> <tr>
<span class="forecast-icon">{{ weatherIcon(day.condition) }}</span> <th></th>
<span class="forecast-condition">{{ day.condition }}</span> <th></th>
<span class="forecast-temps">{{ day.high }}° / {{ day.low }}°</span> <th>Hi / Lo</th>
<span v-if="day.precip_probability != null && day.precip_probability > 0" class="forecast-precip"> <th>💧</th>
💧 {{ day.precip_probability }}% <th>💨 {{ weather.wind_unit ?? 'km/h' }}</th>
</span> </tr>
<span v-else-if="day.precip_mm != null && day.precip_mm > 0" class="forecast-precip"> </thead>
💧 {{ day.precip_mm.toFixed(1) }}mm <tbody>
</span> <tr v-for="day in weather.forecast" :key="day.day">
<span v-else class="forecast-precip forecast-precip--dry">💧 </span> <td class="forecast-day-name">{{ day.day }}</td>
<span class="forecast-wind">💨 {{ day.windspeed_max }} {{ weather.wind_unit ?? 'km/h' }}</span> <td class="forecast-icon">{{ weatherIcon(day.condition) }}</td>
</div> <td class="forecast-temps">{{ day.high }}° / {{ day.low }}°</td>
</div> <td class="forecast-precip" :class="{ 'forecast-precip--dry': !(day.precip_probability != null && day.precip_probability > 0) && !(day.precip_mm != null && day.precip_mm > 0) }">
<template v-if="day.precip_probability != null && day.precip_probability > 0">{{ day.precip_probability }}%</template>
<template v-else-if="day.precip_mm != null && day.precip_mm > 0">{{ day.precip_mm.toFixed(1) }}mm</template>
<template v-else>&mdash;</template>
</td>
<td class="forecast-wind">{{ day.windspeed_max }}</td>
</tr>
</tbody>
</table>
</div> </div>
<div v-else class="weather-card weather-unavailable"> <div v-else class="weather-card weather-unavailable">
Weather data unavailable will retry at next slot. Weather data unavailable will retry at next slot.
@@ -169,20 +177,31 @@ const fetchedAtLabel = computed(() => {
} }
.weather-forecast { .weather-forecast {
display: flex; width: 100%;
gap: 0.75rem; border-collapse: collapse;
overflow-x: auto; margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
font-size: 0.8rem;
} }
.weather-forecast-day { .weather-forecast thead th {
display: flex; font-size: 0.68rem;
flex-direction: column; font-weight: 600;
align-items: center; color: var(--color-text-muted);
gap: 0.25rem; text-align: right;
min-width: 4.5rem; padding: 0.5rem 0.4rem 0.25rem;
font-size: 0.8rem; white-space: nowrap;
}
.weather-forecast thead th:first-child,
.weather-forecast thead th:nth-child(2) {
text-align: left;
}
.weather-forecast tbody td {
padding: 0.3rem 0.4rem;
white-space: nowrap;
vertical-align: middle;
} }
.forecast-day-name { .forecast-day-name {
@@ -190,26 +209,16 @@ const fetchedAtLabel = computed(() => {
} }
.forecast-icon { .forecast-icon {
font-size: 1.2rem; font-size: 1.1rem;
line-height: 1; line-height: 1;
} }
.forecast-condition {
font-size: 0.7rem;
color: var(--color-text-muted);
text-align: center;
line-height: 1.2;
word-break: break-word;
}
.forecast-temps { .forecast-temps {
white-space: nowrap; text-align: right;
} }
.forecast-precip, .forecast-precip {
.forecast-wind { text-align: right;
font-size: 0.72rem;
white-space: nowrap;
color: var(--color-text-muted); color: var(--color-text-muted);
} }
@@ -217,6 +226,11 @@ const fetchedAtLabel = computed(() => {
opacity: 0.35; opacity: 0.35;
} }
.forecast-wind {
text-align: right;
color: var(--color-text-muted);
}
.weather-unavailable { .weather-unavailable {
color: var(--color-text-muted); color: var(--color-text-muted);
font-style: italic; font-style: italic;
+27 -55
View File
@@ -261,22 +261,7 @@ onMounted(async () => {
</div> </div>
</header> </header>
<!-- Left column: Weather --> <!-- Left column: Chat -->
<div class="briefing-left">
<div class="panel-label">Weather</div>
<!-- Forecast card -->
<template v-if="weatherData.length">
<WeatherCard
v-for="loc in weatherData"
:key="(loc as WeatherData).location"
:weather="loc"
:temp-unit="tempUnit"
/>
</template>
<div v-else class="panel-empty">No weather configured</div>
</div>
<!-- Center column: Chat -->
<div class="briefing-center"> <div class="briefing-center">
<ChatPanel <ChatPanel
variant="full" variant="full"
@@ -287,8 +272,19 @@ onMounted(async () => {
/> />
</div> </div>
<!-- Right column: News --> <!-- Right column: Weather + News -->
<div class="briefing-right"> <div class="briefing-right">
<!-- Weather section -->
<template v-if="weatherData.length">
<WeatherCard
v-for="loc in weatherData"
:key="(loc as WeatherData).location"
:weather="loc"
:temp-unit="tempUnit"
/>
</template>
<!-- News section -->
<div class="panel-label-row"> <div class="panel-label-row">
<div class="panel-label">Today's News</div> <div class="panel-label">Today's News</div>
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span> <span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
@@ -348,7 +344,7 @@ onMounted(async () => {
.briefing-shell { .briefing-shell {
display: grid; display: grid;
grid-template-columns: 1fr 2fr 1fr; grid-template-columns: 1fr minmax(320px, 420px);
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
@@ -421,27 +417,10 @@ onMounted(async () => {
} }
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; } .btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
/* ─── Left column (Weather) ──────────────────────────────────────────────── */ /* ─── Left column (Chat) ─────────────────────────────────────────────────── */
.briefing-left {
grid-column: 1;
grid-row: 2;
border-right: 1px solid var(--color-border);
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.briefing-left :deep(.weather-card) {
margin-bottom: 0;
}
/* ─── Center column (Chat) ───────────────────────────────────────────────── */
.briefing-center { .briefing-center {
grid-column: 2; grid-column: 1;
grid-row: 2; grid-row: 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -453,10 +432,10 @@ onMounted(async () => {
min-height: 0; min-height: 0;
} }
/* ─── Right column (News) ────────────────────────────────────────────────── */ /* ─── Right column (Weather + News) ──────────────────────────────────────── */
.briefing-right { .briefing-right {
grid-column: 3; grid-column: 2;
grid-row: 2; grid-row: 2;
border-left: 1px solid var(--color-border); border-left: 1px solid var(--color-border);
overflow-y: auto; overflow-y: auto;
@@ -466,6 +445,10 @@ onMounted(async () => {
gap: 0.5rem; gap: 0.5rem;
} }
.briefing-right :deep(.weather-card) {
margin-bottom: 0.25rem;
}
.panel-label { .panel-label {
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 700; font-weight: 700;
@@ -578,32 +561,21 @@ a.news-title:hover { text-decoration: underline; color: var(--color-primary); }
/* ─── Responsive ─────────────────────────────────────────────────────────── */ /* ─── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 900px) { @media (max-width: 700px) {
.briefing-shell { .briefing-shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr auto; grid-template-rows: auto 1fr auto;
}
.briefing-header {
grid-column: 1;
grid-row: 1;
}
.briefing-left {
grid-column: 1;
grid-row: 2;
border-right: none;
border-bottom: 1px solid var(--color-border);
max-height: 220px;
} }
.briefing-center { .briefing-center {
grid-column: 1; grid-column: 1;
grid-row: 3; grid-row: 2;
} }
.briefing-right { .briefing-right {
grid-column: 1; grid-column: 1;
grid-row: 4; grid-row: 3;
border-left: none; border-left: none;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
max-height: 260px; max-height: 300px;
} }
} }
</style> </style>