jarvis-assist/docs/superpowers/plans/2026-09-12-weather-widget.md

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 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:

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: apiFetch from ./api (existing), GET /api/v1/weather (Task 1).

  • Produces: WeatherWidget default export, no props, rendered inside the existing <nav className="nav"> in App.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)