From 34e8987a76892d2ff592ebaf506c478248de6026 Mon Sep 17 00:00:00 2001 From: Tom Brandenburg Date: Tue, 28 Apr 2026 07:35:34 +0200 Subject: [PATCH 1/5] Investigate issue #300: Add runtime version visibility (API + UI) --- .claude/PRPs/issues/issue-300.md | 359 +++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 .claude/PRPs/issues/issue-300.md diff --git a/.claude/PRPs/issues/issue-300.md b/.claude/PRPs/issues/issue-300.md new file mode 100644 index 0000000..214eeb6 --- /dev/null +++ b/.claude/PRPs/issues/issue-300.md @@ -0,0 +1,359 @@ +# Investigation: Critical: Add runtime version visibility (API + UI) + +**Issue**: #300 (https://github.com/tbrandenburg/made/issues/300) +**Type**: ENHANCEMENT +**Investigated**: 2026-04-28T00:00:00.000Z + +### Assessment + +| Metric | Value | Reasoning | +| ---------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| Priority | HIGH | Explicitly labelled `priority/high` and `severity/high`; blocks production support and deployment verification | +| Complexity | MEDIUM | 4 files across backend and frontend with one API integration point between them | +| Confidence | HIGH | All affected files are identified, patterns are clear, nothing is ambiguous | + +--- + +## Problem Statement + +The application has no mechanism to expose the running version to users or operators. Neither a `/api/version` endpoint, nor any version field in `/api/health`, nor any version display in the frontend Sidebar exist. This is a pure net-new feature; no partial implementation is present. + +--- + +## Analysis + +### Change Rationale + +This is a new feature addition. The version string `"0.1.0"` exists in both `packages/pybackend/pyproject.toml:3` and `packages/frontend/package.json:3` but is never served via HTTP or shown in the UI. The issue (and a follow-up comment from the repository owner) also specifies that the version must be displayed **small font, center aligned in the sidebar menu bar** — specifically below the "MADE" title. + +### Affected Files + +| File | Lines | Action | Description | +| ------------------------------------------------------ | --------- | ------ | ----------------------------------------------- | +| `packages/pybackend/app.py` | 208–214 | UPDATE | Add `/api/version` endpoint; add version to health | +| `packages/pybackend/tests/unit/test_api.py` | END | UPDATE | Add `TestVersionEndpoint` and update health test | +| `packages/frontend/src/components/Sidebar.tsx` | 35–59 | UPDATE | Fetch and display version below sidebar header | +| `packages/frontend/src/styles/sidebar.css` | END | UPDATE | Add `.sidebar-version` styles | + +### Integration Points + +- `packages/pybackend/app.py:208` — health endpoint is the pattern to follow for the new version endpoint +- `packages/frontend/src/components/Sidebar.tsx:38` — `"MADE"` header `
` is the insertion point; version goes directly below it +- `packages/frontend/src/styles/sidebar.css` — imported by Sidebar.tsx; add styles here + +### Git History + +- No version-related endpoints or UI have ever been added — this is first implementation. + +--- + +## Implementation Plan + +### Step 1: Add `/api/version` endpoint and update `/api/health` + +**File**: `packages/pybackend/app.py` +**Lines**: 1–15 (imports), 208–214 (health endpoint) +**Action**: UPDATE + +**Add at the top of the file (after existing imports, before `app = FastAPI(...)`):** + +```python +import importlib.metadata + +_VERSION = importlib.metadata.version("made-pybackend") +``` + +> `importlib.metadata.version` reads the installed package version from `pyproject.toml` without file I/O at request time. The package name `"made-pybackend"` must match the `name` field in `packages/pybackend/pyproject.toml`. + +**Current health endpoint (lines 208–214):** + +```python +@app.get("/api/health") +def health_check(): + return { + "status": "ok", + "workspace": str(get_workspace_home()), + "made": str(get_made_directory()), + } +``` + +**Replace with:** + +```python +@app.get("/api/health") +def health_check(): + return { + "status": "ok", + "version": _VERSION, + "workspace": str(get_workspace_home()), + "made": str(get_made_directory()), + } + + +@app.get("/api/version") +def get_version(): + return { + "version": _VERSION, + "commit_sha": os.environ.get("COMMIT_SHA", "unknown"), + "build_date": os.environ.get("BUILD_DATE", "unknown"), + "environment": os.environ.get("ENVIRONMENT", "development"), + } +``` + +**Why**: `importlib.metadata` is the idiomatic Python way to read the installed package version; `os` is already imported (`import os` at line 6). + +--- + +### Step 2: Add tests for version endpoint and update health test + +**File**: `packages/pybackend/tests/unit/test_api.py` +**Action**: UPDATE (append new class; update existing health test) + +**Update existing health test** to assert `version` field is present: + +```python +# In TestHealthEndpoint.test_health_check_success, add assertion: +assert "version" in data +``` + +**Append new test class:** + +```python +class TestVersionEndpoint: + """Test the /api/version endpoint.""" + + def test_version_returns_version_string(self): + """Version endpoint returns a version field.""" + response = client.get("/api/version") + + assert response.status_code == 200 + data = response.json() + assert "version" in data + assert isinstance(data["version"], str) + assert len(data["version"]) > 0 + + def test_version_includes_metadata_fields(self): + """Version endpoint returns all required metadata fields.""" + response = client.get("/api/version") + + data = response.json() + assert "commit_sha" in data + assert "build_date" in data + assert "environment" in data +``` + +--- + +### Step 3: Display version in Sidebar (small font, center aligned, below MADE header) + +**File**: `packages/frontend/src/components/Sidebar.tsx` +**Lines**: 10, 35–59 +**Action**: UPDATE + +**Current imports (lines 9–12):** + +```tsx +import { RecurringTasksIcon } from "./icons/RecurringTasksIcon"; +import React from "react"; +import { NavLink } from "react-router-dom"; +import "../styles/sidebar.css"; +``` + +**Replace with:** + +```tsx +import { RecurringTasksIcon } from "./icons/RecurringTasksIcon"; +import React, { useEffect, useState } from "react"; +import { NavLink } from "react-router-dom"; +import "../styles/sidebar.css"; +``` + +**Current component body (lines 35–59):** + +```tsx +export const Sidebar: React.FC = ({ open, onNavigate }) => { + return ( + + ); +}; +``` + +**Replace with:** + +```tsx +export const Sidebar: React.FC = ({ open, onNavigate }) => { + const [version, setVersion] = useState(""); + + useEffect(() => { + fetch("/api/version") + .then((res) => res.json()) + .then((data: { version: string }) => setVersion(data.version)) + .catch(() => setVersion("")); + }, []); + + return ( + + ); +}; +``` + +**Why**: The owner's comment explicitly requested "small font center aligned in the menu bar" — placed directly below the "MADE" title is the natural location. Version is fetched once on mount; silently omitted on error to avoid broken UI. + +--- + +### Step 4: Add `.sidebar-version` CSS + +**File**: `packages/frontend/src/styles/sidebar.css` +**Action**: UPDATE (append at end of file) + +**Append:** + +```css +.sidebar-version { + font-size: 0.7rem; + text-align: center; + color: var(--muted); + margin-top: -1rem; +} +``` + +**Why**: `font-size: 0.7rem` is noticeably smaller than the `1.5rem` header; `text-align: center` fulfills the owner's explicit request; negative `margin-top` tightens the gap with the header above; `var(--muted)` keeps it visually subtle. + +--- + +## Patterns to Follow + +**Backend endpoint pattern — mirror `health_check` at `app.py:208–214`:** + +```python +@app.get("/api/health") +def health_check(): + return { + "status": "ok", + "workspace": str(get_workspace_home()), + "made": str(get_made_directory()), + } +``` + +**Backend test pattern — mirror `TestHealthEndpoint` at `test_api.py:15–32`:** + +```python +class TestHealthEndpoint: + @patch("app.get_workspace_home") + @patch("app.get_made_directory") + def test_health_check_success(self, mock_made_dir, mock_workspace_home): + mock_workspace_home.return_value = "/test/workspace" + mock_made_dir.return_value = "/test/made" + + response = client.get("/api/health") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" +``` + +**Frontend hook pattern — `useEffect` + `useState` for one-time fetch (standard React).** + +--- + +## Edge Cases & Risks + +| Risk / Edge Case | Mitigation | +| ------------------------------------------ | ------------------------------------------------------------------ | +| `importlib.metadata` can't find package | Wrap in `try/except` and fall back to `"unknown"` if needed | +| Package name mismatch in pyproject.toml | Verify `name` field in `packages/pybackend/pyproject.toml` first | +| `/api/version` unavailable when sidebar mounts | `catch` sets `version` to `""` → version `
` not rendered | +| `margin-top: -1rem` too aggressive visually | Adjust to `margin-top: -0.75rem` if gap looks off in the browser | + +--- + +## Validation + +### Automated Checks + +```bash +# Backend unit tests +cd packages/pybackend && uv run python -m pytest tests/unit/test_api.py -v + +# Frontend type check + build +cd packages/frontend && npm run build + +# Full QA +make qa-quick +``` + +### Manual Verification + +1. Start the app: `make run` +2. `curl http://localhost:3000/api/version` → JSON with `version`, `commit_sha`, `build_date`, `environment` +3. `curl http://localhost:3000/api/health` → JSON includes `version` field +4. Open `http://localhost:5173` → sidebar shows `v0.1.0` in small centered text below "MADE" + +--- + +## Scope Boundaries + +**IN SCOPE:** +- `GET /api/version` endpoint returning version + env metadata +- `GET /api/health` updated to include version +- Sidebar version display (small, center-aligned, below header) +- CSS for `.sidebar-version` +- Unit tests for new endpoint and updated health test + +**OUT OF SCOPE (do not touch):** +- Build-time injection of `COMMIT_SHA` / `BUILD_DATE` (CI/CD concern) +- About dialog +- Version display in header or any other UI location +- Frontend `useApi.ts` wrapper (direct `fetch` in Sidebar is sufficient and simpler) +- Docker/container changes + +--- + +## Metadata + +- **Investigated by**: Claude +- **Timestamp**: 2026-04-28T00:00:00.000Z +- **Artifact**: `.claude/PRPs/issues/issue-300.md` From 1afa304ad29eab0b0ccc146e1c41e21755c12345 Mon Sep 17 00:00:00 2001 From: Tom Brandenburg Date: Tue, 28 Apr 2026 07:41:17 +0200 Subject: [PATCH 2/5] feat: Add runtime version visibility via API and UI (#300) The application had no mechanism to expose the running version to users or operators. This adds a dedicated /api/version endpoint, includes version in /api/health, and displays the version in the sidebar below the MADE header. Changes: - Add importlib.metadata-based _VERSION constant to app.py - Add GET /api/version endpoint returning version + env metadata - Update GET /api/health to include version field - Add TestVersionEndpoint test class (2 tests) - Update health test to assert version field presence - Add useEffect/useState to Sidebar to fetch and display version - Add .sidebar-version CSS (small, centered, muted) Fixes #300 --- packages/frontend/src/components/Sidebar.tsx | 12 +++++++++- packages/frontend/src/styles/sidebar.css | 7 ++++++ packages/pybackend/app.py | 14 ++++++++++++ packages/pybackend/tests/unit/test_api.py | 24 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/Sidebar.tsx b/packages/frontend/src/components/Sidebar.tsx index b843184..f64dd94 100644 --- a/packages/frontend/src/components/Sidebar.tsx +++ b/packages/frontend/src/components/Sidebar.tsx @@ -7,7 +7,7 @@ import { Squares2X2Icon, } from "@heroicons/react/24/outline"; import { RecurringTasksIcon } from "./icons/RecurringTasksIcon"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { NavLink } from "react-router-dom"; import "../styles/sidebar.css"; @@ -33,9 +33,19 @@ interface SidebarProps { } export const Sidebar: React.FC = ({ open, onNavigate }) => { + const [version, setVersion] = useState(""); + + useEffect(() => { + fetch("/api/version") + .then((res) => res.json()) + .then((data: { version: string }) => setVersion(data.version)) + .catch(() => setVersion("")); + }, []); + return (