feat(web): integrate crop stress into Pulse page
- index() route: add get_weather_stress_latest() and get_weather_stress_trend(90d) to asyncio.gather; pass weather_stress_latest and weather_stress_trend to template - pulse.html: add 5th metric card (Crop Stress Index, color-coded green/copper/danger) - pulse.html: add 5th sparkline card (90d avg stress trend) linking to /dashboard/weather - pulse.html: update spark-grid to auto-fit (minmax 280px) to accommodate 5 cards - pulse.html: add Weather freshness badge to the freshness bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,16 +25,13 @@
|
||||
color: var(--color-stone);
|
||||
}
|
||||
|
||||
/* 2×2 sparkline grid */
|
||||
/* Sparkline grid — auto-fit for 4 or 5 cards */
|
||||
.spark-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.spark-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.spark-card {
|
||||
background: white;
|
||||
@@ -94,7 +91,7 @@
|
||||
<p>Global coffee market — full picture in 10 seconds{% if user.name %} · Welcome back, {{ user.name }}{% endif %}</p>
|
||||
</div>
|
||||
|
||||
<!-- 4 metric cards -->
|
||||
<!-- Metric cards -->
|
||||
<div class="grid-4 mb-6 pulse-in">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">KC=F Close</div>
|
||||
@@ -139,6 +136,17 @@
|
||||
</div>
|
||||
<div class="metric-sub">ending stocks / consumption{% if stu_latest %} · {{ stu_latest.market_year }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Crop Stress Index</div>
|
||||
<div class="metric-value {% if weather_stress_latest and weather_stress_latest.avg_crop_stress_index > 40 %}text-danger{% elif weather_stress_latest and weather_stress_latest.avg_crop_stress_index > 20 %}text-copper{% else %}text-bean-green{% endif %}">
|
||||
{% if weather_stress_latest %}{{ "{:.0f}".format(weather_stress_latest.avg_crop_stress_index) }}/100{% else %}--{% endif %}
|
||||
</div>
|
||||
<div class="metric-sub">
|
||||
avg · {% if weather_stress_latest %}{{ weather_stress_latest.locations_under_stress }} stressed{% endif %}
|
||||
{% if weather_stress_latest %} · {{ weather_stress_latest.observation_date }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Freshness bar -->
|
||||
@@ -156,6 +164,9 @@
|
||||
<a href="{{ url_for('public.methodology') }}" class="freshness-badge">
|
||||
<strong>ICE Stocks</strong> {% if ice_stocks_latest %}{{ ice_stocks_latest.report_date }}{% else %}—{% endif %}
|
||||
</a>
|
||||
<a href="{{ url_for('dashboard.weather') }}" class="freshness-badge">
|
||||
<strong>Weather</strong> {% if weather_stress_latest %}{{ weather_stress_latest.observation_date }}{% else %}—{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 2×2 sparkline grid -->
|
||||
@@ -209,6 +220,18 @@
|
||||
<div class="spark-meta">USDA WASDE · 1,000 60-kg bags · production vs distribution</div>
|
||||
</div>
|
||||
|
||||
<!-- Weather Crop Stress 90d -->
|
||||
<div class="spark-card">
|
||||
<div class="spark-card-head">
|
||||
<div class="spark-title">Crop Stress Index — 90 days</div>
|
||||
<a href="{{ url_for('dashboard.weather') }}" class="spark-detail-link">View details →</a>
|
||||
</div>
|
||||
<div class="spark-chart-wrap">
|
||||
<canvas id="sparkWeather"></canvas>
|
||||
</div>
|
||||
<div class="spark-meta">Open-Meteo ERA5 · avg across 12 origins · 0 = no stress · 100 = severe</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if plan == "free" %}
|
||||
@@ -338,6 +361,27 @@
|
||||
options: sparkOpts(C.copper, false),
|
||||
});
|
||||
}
|
||||
|
||||
// Weather crop stress sparkline (90d)
|
||||
var weatherRaw = {{ weather_stress_trend | tojson }};
|
||||
if (weatherRaw && weatherRaw.length > 0) {
|
||||
var weatherAsc = weatherRaw.slice().reverse();
|
||||
new Chart(document.getElementById('sparkWeather'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: weatherAsc.map(function (r) { return r.observation_date; }),
|
||||
datasets: [{
|
||||
data: weatherAsc.map(function (r) { return r.avg_crop_stress_index; }),
|
||||
borderColor: C.copper,
|
||||
backgroundColor: C.copper + '22',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
}],
|
||||
},
|
||||
options: sparkOpts(C.copper, true),
|
||||
});
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user