# 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 the `weather_cache` table already exist and are verified working (one row present from a manual test run) — this plan does not touch them. - `weather_cache` schema: `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 (see `Claude outputs/main.py`). --- ### Task 1: `GET /api/v1/weather` endpoint **Files:** - Modify: `Claude outputs/main.py` (add `get_latest_weather` DB helper + `GET /api/v1/weather` endpoint) - Test: `Claude outputs/tests/test_weather.py` **Interfaces:** - Produces: `GET /api/v1/weather` returning `{"location": str, "temperature_c": float, "condition_text": str, "fetched_at": str}` on success, `503` when `weather_cache` is empty. Consumed by Task 2's `WeatherWidget`. - [ ] **Step 1: Write the failing test** Create `Claude outputs/tests/test_weather.py`: ```python 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** ```bash 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`): ```python 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`): ```python @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** ```bash 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: ```bash 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: `apiFetch` from `./api` (existing), `GET /api/v1/weather` (Task 1). - Produces: `WeatherWidget` default export, no props, rendered inside the existing `