10 KiB
JARVIS Weather Widget Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Expose the weather data the new n8n workflow writes to Postgres via a protected API endpoint, and show it top-right in the frontend nav bar.
Architecture: GET /api/v1/weather reads the newest row from weather_cache (already populated by the "Wetter Crailsheim" n8n workflow, verified working). A new WeatherWidget React component polls that endpoint and renders in the nav bar.
Tech Stack: FastAPI (existing db_query/require_admin_key patterns), React (existing apiFetch pattern).
Spec: docs/superpowers/specs/2026-09-12-weather-widget-design.md
Global Constraints
- No dedicated git repository for this project (see prior plans) — every "Commit" step is replaced by "confirm the file is saved".
- SSH:
ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98. Local backend files:Claude outputs/(quote the path). Local frontend files:web/. - The n8n workflow "Wetter Crailsheim" (id
BwNCJ2TkuZfqUzst), its Postgres credential "JARVIS Postgres", and theweather_cachetable already exist and are verified working (one row present from a manual test run) — this plan does not touch them. weather_cacheschema:id, location, temperature_c NUMERIC, condition_code INTEGER, condition_text VARCHAR(100), fetched_at TIMESTAMP.- The endpoint must use
dependencies=[Depends(require_admin_key)]like every other/api/v1/*route (seeClaude outputs/main.py).
Task 1: GET /api/v1/weather endpoint
Files:
- Modify:
Claude outputs/main.py(addget_latest_weatherDB helper +GET /api/v1/weatherendpoint) - Test:
Claude outputs/tests/test_weather.py
Interfaces:
-
Produces:
GET /api/v1/weatherreturning{"location": str, "temperature_c": float, "condition_text": str, "fetched_at": str}on success,503whenweather_cacheis empty. Consumed by Task 2'sWeatherWidget. -
Step 1: Write the failing test
Create Claude outputs/tests/test_weather.py:
import os
import sys
from datetime import datetime
from unittest.mock import AsyncMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import pytest
from fastapi.testclient import TestClient
import main
main.API_KEY_ADMIN = "test-secret"
HEADERS = {"X-Admin-Key": "test-secret"}
@pytest.fixture()
def client():
with TestClient(main.app) as c:
yield c
def test_weather_returns_503_when_no_data(client):
with patch.object(main, "pg_pool", "not-none"), patch.object(
main, "get_latest_weather", new=AsyncMock(return_value=None)
):
response = client.get("/api/v1/weather", headers=HEADERS)
assert response.status_code == 503
def test_weather_returns_latest_row(client):
row = {
"location": "Crailsheim",
"temperature_c": 18.4,
"condition_text": "Bewoelkt",
"fetched_at": datetime(2026, 9, 12, 16, 0, 3, 123456),
}
with patch.object(main, "pg_pool", "not-none"), patch.object(
main, "get_latest_weather", new=AsyncMock(return_value=row)
):
response = client.get("/api/v1/weather", headers=HEADERS)
assert response.status_code == 200
assert response.json()["location"] == "Crailsheim"
def test_weather_requires_admin_key(client):
response = client.get("/api/v1/weather")
assert response.status_code == 401
Note: patch.object(main, "pg_pool", "not-none") stands in for "database configured" — the endpoint only checks if not pg_pool, so any truthy value works without needing a real connection pool.
- Step 2: Run test to verify it fails
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
ssh $SSHOPTS jarvis-core@72.61.186.98 "mkdir -p /tmp/jarvis-test/tests"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" "$OUT/requirements.txt" "$OUT/requirements-dev.txt" jarvis-core@72.61.186.98:/tmp/jarvis-test/
scp $SSHOPTS "$OUT/tests/test_weather.py" jarvis-core@72.61.186.98:/tmp/jarvis-test/tests/
ssh $SSHOPTS jarvis-core@72.61.186.98 "docker run --rm -v /tmp/jarvis-test:/app -w /app python:3.11-slim bash -c 'pip install -q -r requirements-dev.txt -r requirements.txt && python -m pytest tests/test_weather.py -v'"
Expected: FAIL — get_latest_weather doesn't exist / /api/v1/weather is 404.
- Step 3: Add the DB helper and endpoint
In Claude outputs/main.py, add near the other DB helpers (after search_chunks):
async def get_latest_weather():
return await db_query(
"""
SELECT location, temperature_c, condition_text, fetched_at
FROM weather_cache ORDER BY fetched_at DESC LIMIT 1
""",
fetch="one",
)
Add the endpoint near the other admin/status endpoints (after detailed_health):
@app.get("/api/v1/weather", dependencies=[Depends(require_admin_key)])
async def get_weather():
"""Latest cached weather reading, populated by the n8n 'Wetter Crailsheim' workflow"""
if not pg_pool:
raise HTTPException(status_code=503, detail="Database is not configured")
row = await get_latest_weather()
if not row:
raise HTTPException(status_code=503, detail="Weather data not available yet")
return {
"location": row["location"],
"temperature_c": float(row["temperature_c"]),
"condition_text": row["condition_text"],
"fetched_at": row["fetched_at"].isoformat(),
}
- Step 4: Run test to verify it passes
Re-run the Step 2 command. Expected: 3 passed.
- Step 5: Deploy and verify against the live API
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py
ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api && sleep 8 && docker logs --tail 15 jarvis-api"
Expected: log ends with Application startup complete. (no traceback). Then:
ADMIN_KEY=$(ssh $SSHOPTS jarvis-core@72.61.186.98 "grep API_KEY_ADMIN /home/jarvis-core/jarvis/.env" | cut -d= -f2-)
ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -H 'X-Admin-Key: $ADMIN_KEY' http://localhost:8000/api/v1/weather; echo"
Expected: JSON with "location":"Crailsheim" and the temperature/condition from the test run done during n8n workflow setup.
- Step 6: Confirm files saved (no git repo for this project — see Global Constraints)
Task 2: WeatherWidget in the nav bar
Files:
- Create:
web/src/components/WeatherWidget.tsx - Modify:
web/src/App.tsx(render the widget in the nav) - Modify:
web/src/index.css(right-align the widget in the nav)
Interfaces:
-
Consumes:
apiFetchfrom./api(existing),GET /api/v1/weather(Task 1). -
Produces:
WeatherWidgetdefault export, no props, rendered inside the existing<nav className="nav">inApp.tsx. -
Step 1: Add the nav CSS for right-alignment
In web/src/index.css, add after the .nav button rule:
.weather-widget { margin-left: auto; font-size: 0.9rem; color: #94a3b8; align-self: center; }
- Step 2: Create the component
Create web/src/components/WeatherWidget.tsx:
import { useEffect, useState } from "react";
import { apiFetch } from "../api";
type Weather = { location: string; temperature_c: number; condition_text: string };
const REFRESH_MS = 5 * 60 * 1000;
export default function WeatherWidget() {
const [weather, setWeather] = useState<Weather | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const response = await apiFetch("/api/v1/weather");
if (!response.ok) {
if (!cancelled) setFailed(true);
return;
}
const data = await response.json();
if (!cancelled) {
setWeather(data);
setFailed(false);
}
} catch {
if (!cancelled) setFailed(true);
}
}
load();
const interval = setInterval(load, REFRESH_MS);
return () => {
cancelled = true;
clearInterval(interval);
};
}, []);
if (failed) {
return <span className="weather-widget">Wetter nicht verfuegbar</span>;
}
if (!weather) {
return null;
}
return (
<span className="weather-widget">
{`${weather.location}: ${weather.temperature_c.toFixed(1)}\u00b0C \u00b7 ${weather.condition_text}`}
</span>
);
}
- Step 3: Render it in the nav bar
In web/src/App.tsx, add the import next to the other component imports:
import WeatherWidget from "./components/WeatherWidget";
Change the <nav> block from:
<nav className="nav">
<button onClick={() => setView("chat")} disabled={view === "chat"}>Chat</button>
<button onClick={() => setView("dashboard")} disabled={view === "dashboard"}>Dashboard</button>
<button onClick={() => logout()}>Logout</button>
</nav>
to:
<nav className="nav">
<button onClick={() => setView("chat")} disabled={view === "chat"}>Chat</button>
<button onClick={() => setView("dashboard")} disabled={view === "dashboard"}>Dashboard</button>
<button onClick={() => logout()}>Logout</button>
<WeatherWidget />
</nav>
- Step 4: Verify the build works
cd "C:\Users\Jonny\Projekte\Claude\JARVIS\web"
npm run build
Expected: exits 0.
- Step 5: Deploy
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS"
scp $SSHOPTS -r "$OUT\web\src" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/web/
ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose build jarvis-web && docker compose up -d jarvis-web"
- Step 6: End-to-end browser verification
Using the claude-in-chrome browser tools: open https://jarvis.mbo-tech-it.de, log in with the real API_KEY_ADMIN, confirm the weather text (e.g. "Crailsheim: 20.9°C · Bewoelkt") appears top-right of the nav bar on both the Chat and Dashboard views.
- Step 7: Confirm files saved (no git repo for this project — see Global Constraints)