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 (