diff --git a/.claude/PRPs/issues/completed/issue-300.md b/.claude/PRPs/issues/completed/issue-300.md new file mode 100644 index 0000000..214eeb6 --- /dev/null +++ b/.claude/PRPs/issues/completed/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` diff --git a/.gitignore b/.gitignore index bd38c8f..090dbd6 100644 --- a/.gitignore +++ b/.gitignore @@ -308,4 +308,6 @@ workspace/ .agents/ # dev files -dev/ \ No newline at end of file +dev/ +!dev/state/ +!dev/state/*.json \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index febea65..ef20211 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,15 @@ Follow this mandatory guideline even if not instructed. Focus on lightweight, fast feedback — only essential tests are included. - Before pushing changes, run `make qa-quick` from repository root and address any failures. +### Pre-push quality gate (mandatory) + +A git `pre-push` hook is stored in `scripts/hooks/pre-push` and installed by `make install`. +It runs `make qa-quick` (lint + format + unit-test) automatically before every `git push`. + +- **Never bypass this hook** (`--no-verify`) unless CI is explicitly broken and you are pushing a hotfix. +- If the hook is missing after a fresh clone, run `make install` or `make install-hooks` to restore it. +- When adding new linting rules, ensure they pass locally via `make lint` before committing. + ### CI/CD Note for Backend Pytest - If CI runs `python -m pytest packages/pybackend/tests/unit` directly, install backend dependencies first (e.g., `cd packages/pybackend && uv sync` or `python -m pip install -e packages/pybackend`) to avoid missing-import collection errors. - If CI runs `python -m pytest` outside the `uv` environment, dependencies like `fastapi` can be missing. Prefer running tests with `uv run` (for example: `uv run --project packages/pybackend python -m pytest packages/pybackend/tests/unit`) or activate the `.venv` created by `uv sync` before executing pytest. @@ -57,6 +66,7 @@ Focus on lightweight, fast feedback — only essential tests are included. - Use **Node.js 18 or newer** for the frontend. - Run `npm install` to install frontend dependencies. - Run `cd packages/pybackend && uv sync` to install backend dependencies. +- **Run `make install` (preferred) — installs all dependencies AND sets up the pre-push git hook.** - Ensure the environment variable `PORT` is respected (default: `3000`). - The app must listen on `0.0.0.0` (not `localhost`) to enable public preview. - The vite configuration has to be set up for allowing following remote hosts for previews (allowedHosts): .ngrok-free.dev, .ngrok.io, .ngrok.app @@ -64,6 +74,10 @@ Focus on lightweight, fast feedback — only essential tests are included. ## Build & Run Instructions 1. **Install dependencies** ```bash + # Install all dependencies and git hooks (recommended) + make install + + # Or manually: # Frontend dependencies npm install diff --git a/Makefile b/Makefile index 7923c0b..774f48d 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ MADE_WORKSPACE_HOME ?= $(abspath $(CURDIR)/workspace/) export MADE_HOME export MADE_WORKSPACE_HOME -.PHONY: help lint format test unit-test system-test qa qa-quick build run stop restart clean install install-node install-pybackend test-coverage security-audit docker-build docker-up docker-down docker-dev docker-clean release tag-release +.PHONY: help lint format test unit-test system-test qa qa-quick build run stop restart clean install install-node install-pybackend install-hooks test-coverage security-audit docker-build docker-up docker-down docker-dev docker-clean release tag-release # Default target help: @@ -43,7 +43,7 @@ help: @echo " restart Stop and then start all services" @echo "" @echo "Maintenance:" - @echo " install Install/sync dependencies" + @echo " install Install/sync dependencies and git hooks" @echo " clean Clean build artifacts and cache" @echo "" @echo "Example usage:" @@ -193,7 +193,7 @@ stop: restart: stop run # Maintenance Tasks -install: install-node install-pybackend +install: install-node install-pybackend install-hooks @echo "✅ Dependencies installed for frontend and Python backend" install-node: @@ -205,6 +205,12 @@ install-pybackend: @echo "⚙️ Syncing Python backend dependencies..." cd $(PYBACKEND_DIR) && uv sync +install-hooks: + @echo "⚙️ Installing git hooks..." + cp scripts/hooks/pre-push .git/hooks/pre-push + chmod +x .git/hooks/pre-push + @echo "✅ Git hooks installed (pre-push runs make qa-quick)" + clean: @echo "🧹 Cleaning build artifacts and cache..." rm -rf node_modules diff --git a/dev/state/task-ledger.json b/dev/state/task-ledger.json index e26f5a4..1f3c9a9 100644 --- a/dev/state/task-ledger.json +++ b/dev/state/task-ledger.json @@ -1,37 +1,30 @@ { - "TASK_1_ANALYZE_PR384": { + "T1": { "status": "done", "evidence": [ - "gh pr checks 384: Quality Assurance (Lint + Format + Unit Tests) = FAIL", - "gh run view 24555659089 --log-failed: python-multipart 0.0.22 has CVE-2026-40347, fix versions: 0.0.26", - "5xWhy: Why1=CI security-audit fails → Why2=pip-audit found CVE-2026-40347 in python-multipart 0.0.22 → Why3=pyproject.toml has python-multipart>=0.0.9 which allows 0.0.22 → Why4=PR only changed CSS, did not update python-multipart constraint → Why5=no automated dep-update process keeps python-multipart current" + "PR #395 analyzed: lint errors found in backend (ruff): 18 errors including unused imports, line length issues", + "make lint output shows: Found 18 errors in packages/pybackend/*.py" ], - "last_verified": "2026-04-17T08:42:00Z" + "last_verified": "2026-04-28T00:00:00Z" }, - "TASK_2_PLAN_FIX_PR384": { - "status": "done", - "evidence": [ - "Plan: bump python-multipart from >=0.0.9 to >=0.0.26 in packages/pybackend/pyproject.toml, run uv sync, verify pip-audit passes" - ], - "last_verified": "2026-04-17T08:42:00Z" + "T2": { + "status": "in_progress", + "evidence": [], + "last_verified": "2026-04-28T00:00:00Z" }, - "TASK_3_IMPLEMENT_FIX_PR384": { - "status": "done", - "evidence": [ - "Edit: python-multipart>=0.0.9 → >=0.0.26 in packages/pybackend/pyproject.toml", - "uv sync: python-multipart 0.0.22 → 0.0.26 installed", - "pip-audit: No known vulnerabilities found" - ], - "last_verified": "2026-04-17T08:44:00Z" + "T3": { + "status": "pending", + "evidence": [], + "last_verified": "2026-04-28T00:00:00Z" }, - "TASK_4_COMMIT_PUSH_PR384": { - "status": "in_progress", + "T4": { + "status": "pending", "evidence": [], - "last_verified": "2026-04-17T08:44:00Z" + "last_verified": "2026-04-28T00:00:00Z" }, - "TASK_5_OBSERVE_CI_PR384": { + "T5": { "status": "pending", "evidence": [], - "last_verified": "2026-04-17T08:44:00Z" + "last_verified": "2026-04-28T00:00:00Z" } } 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 (