Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 21 additions & 98 deletions apps/delta-command/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Delta Command

A polished engineering operations dashboard for delta engineering teams. Track the opportunity pipeline, team utilization, and active project delivery — built for both engineers and leadership.
An engineering operations dashboard for delta engineering teams. Tracks opportunity pipeline, team utilization, and active project delivery.

## Architecture

Expand All @@ -13,116 +13,39 @@ A polished engineering operations dashboard for delta engineering teams. Track t

## Quick Start

### 1. Backend (Python + UV)

```bash
cd apps/delta-command/backend
uv sync
uv run delta-command
```

API runs at http://127.0.0.1:8000

### 2. Frontend (Next.js)

In a second terminal:

```bash
cd apps/delta-command
npm install
npm run dev
```

Open http://localhost:3000 — API requests are proxied to the Python backend.

Or use the Makefile from `apps/delta-command/`:

```bash
make install # install both
make backend # start API
make frontend # start UI
make test # run backend tests + frontend build
```

## Database

All data lives in **one JSON file**:
# Backend (in one terminal)
cd apps/delta-command/backend && uv sync && uv run delta-command # → :8000

# Frontend (in another)
cd apps/delta-command && npm install && npm run dev # → :3000
```
backend/config/data.json
```

Every API read and write uses this file. UI edits (opportunity stages, project status, utilization timeline cells) are saved directly to disk via atomic JSON writes.

Customize your team by editing `data.json`, or regenerate a 30-engineer roster:
Or use the Makefile:

```bash
cd backend && uv run python scripts/seed_team.py
make install # install both
make backend # start API
make frontend # start UI
make test # backend tests + frontend build
```

### Example JSON structure

```json
{
"lastUpdated": "2026-07-04T12:00:00Z",
"engineers": [
{
"id": "eng-1",
"name": "Sarah Chen",
"role": "Principal Engineer",
"utilization": 95,
"status": "allocated",
"skills": ["Architecture", "Cloud"],
"currentProjects": ["proj-1"]
}
],
"opportunities": [
{
"id": "opp-1",
"title": "Enterprise Data Platform",
"client": "Meridian Financial",
"stage": "negotiation",
"value": 850000,
"probability": 75
}
],
"projects": [
{
"id": "proj-1",
"name": "Core Banking Migration",
"status": "active",
"progress": 68,
"milestones": [
{
"id": "ms-1",
"title": "Architecture Sign-off",
"dueDate": "2026-02-15",
"completed": true
}
]
}
]
}
```
## How it works

Persistence is handled by `delta_command/json_db.py` (I/O) and `delta_command/store.py` (domain operations).
The whole app is small on purpose:

## Features
- **Backend** is a thin CRUD layer with 4 endpoints (see `backend/README.md`). It never computes derived metrics.
- **Frontend** fetches the full database via `GET /api/state` once per page and derives every KPI, chart, and insight on the client.
- **Data model** is `Engineer | Opportunity | Project` in `backend/config/data.json`.
- **Mutations** (opportunity stage, project status, timeline cells) `PATCH` the JSON file atomically and return the updated state.

- **Executive Dashboard** — KPIs, pipeline funnel, utilization charts, at-risk alerts
- **Opportunity Pipeline** — Kanban board with stage management (persisted)
- **Team Utilization** — Editable timeline grid (persisted)
- **Project Execution** — Progress, budget, milestone tracking with status updates (persisted)

## Environment Variables
## Environment

| Variable | Default | Description |
|----------|---------|-------------|
| `DELTA_API_URL` | `http://127.0.0.1:8000` | Backend URL (Next.js server-side) |
| `DELTA_DATA_PATH` | `backend/config/data.json` | JSON database file |
| `DELTA_HOST` / `DELTA_PORT` | `127.0.0.1` / `8000` | API bind address |

`DELTA_CONFIG_PATH` is supported as an alias for `DELTA_DATA_PATH`.
| `DELTA_API_URL` | `http://127.0.0.1:8000` | Backend URL (used by Next.js server-side and rewrites). |
| `DELTA_DATA_PATH` | `backend/config/data.json` | JSON database file. |
| `DELTA_HOST` / `DELTA_PORT` | `127.0.0.1` / `8000` | Backend bind address. |

## Production

Expand All @@ -134,4 +57,4 @@ cd backend && uv sync --no-dev && uv run uvicorn delta_command.main:app --host 0
npm run build && npm start
```

For production at scale, replace the JSON file store with PostgreSQL by extending `delta_command/store.py` — the Pydantic models and API routes can stay the same.
For scale, swap the JSON file store in `delta_command/store.py` for a real database — the Pydantic models and API routes stay the same.
2 changes: 0 additions & 2 deletions apps/delta-command/backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ __pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
config/runtime.json
config/runtime.yaml
38 changes: 27 additions & 11 deletions apps/delta-command/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
# Delta Command Backend

Python API for Delta Command, backed by a single JSON file database.
A thin FastAPI CRUD layer over a single JSON file. Four endpoints total.

## Setup

```bash
cd apps/delta-command/backend
uv sync
uv run delta-command # → http://127.0.0.1:8000
```

## Run
## API

```bash
uv run delta-command
```

API available at http://127.0.0.1:8000
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/state` | Return the entire database (`engineers`, `opportunities`, `projects`, `lastUpdated`). The frontend derives every metric from this. |
| PATCH | `/api/opportunities` | Update stage (probability auto-set to 100/0 for won/lost). |
| PATCH | `/api/projects` | Update status. |
| PATCH | `/api/timeline` | Update one weekly utilization cell for one engineer; recomputes engineer.utilization and status. |

## Database

All data is stored in **`config/data.json`** — one file for reads and writes.
All data lives in **`config/data.json`**. Set `DELTA_DATA_PATH` to override the path.

Writes are atomic (`delta_command/json_db.py`).

Regenerate the 30-engineer seed dataset:

UI changes (opportunity stages, project status, utilization timeline) persist directly to this file.
```bash
uv run python scripts/seed_team.py
```

`scripts/seed_team.py` is the single source of truth for timeline shape — the API only ever updates existing cells, it never adds new weeks.

Override the path with `DELTA_DATA_PATH`.
## Layout

Persistence uses atomic JSON writes via `delta_command/json_db.py`.
```
delta_command/
├── json_db.py # atomic file I/O
├── models.py # Pydantic models (snake_case in Python, camelCase in JSON)
├── store.py # domain operations (load/save/update)
└── main.py # FastAPI routes
```
64 changes: 31 additions & 33 deletions apps/delta-command/backend/scripts/seed_team.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Generate a 30-engineer seed dataset for config/data.json."""
"""Regenerate config/data.json with a fresh 30-engineer roster.

from __future__ import annotations
This is the single source of truth for timeline shape: every engineer gets an
8-week utilization timeline seeded here, and the API only ever mutates cells
that already exist.
"""

import json
import random
Expand All @@ -17,11 +20,11 @@
"Nguyen", "Cooper", "Tanaka", "Silva", "Martin", "Rivera", "Brooks", "Clark", "Williams", "Carter",
"Davis", "Evans", "Ali", "Murphy", "Lee", "Petrov", "Santos", "Wright", "Zhang", "Adams",
]
ROLES = [
ROLE_WEIGHTS = [
("Engineer", 0.35),
("Senior Engineer", 0.35),
("Staff Engineer", 0.15),
("Principal Engineer", 0.1),
("Principal Engineer", 0.10),
("Engineering Manager", 0.05),
]
SKILLS = [
Expand All @@ -30,63 +33,58 @@
"Frontend", "CI/CD", "PostgreSQL",
]
PROJECT_IDS = [f"proj-{i}" for i in range(1, 11)]
TIMELINE_WEEKS = 8


def pick_role(rng: random.Random) -> str:
def _pick_role(rng: random.Random) -> str:
roll = rng.random()
cumulative = 0.0
for role, weight in ROLES:
for role, weight in ROLE_WEIGHTS:
cumulative += weight
if roll <= cumulative:
return role
return "Engineer"


def week_start(value: date) -> date:
return value - timedelta(days=value.weekday())


def build_timeline(base: int, rng: random.Random) -> list[dict]:
today = date.today()
current = week_start(today)
start = current - timedelta(weeks=2)
timeline: list[dict] = []
for index in range(8):
week = start + timedelta(weeks=index)
drift = (index - 2) * 3
value = max(0, min(150, base + drift - (index % 3) * 5 + rng.randint(-2, 2)))
timeline.append({"weekStart": week.isoformat(), "utilization": value, "note": None})
return timeline


def status_for(utilization: int) -> str:
def _status_for(utilization: int) -> str:
if utilization >= 100:
return "overallocated"
if utilization >= 50:
return "allocated"
return "available"


def _timeline(base: int, rng: random.Random) -> list[dict]:
monday = date.today() - timedelta(days=date.today().weekday())
start = monday - timedelta(weeks=2)
cells = []
for i in range(TIMELINE_WEEKS):
drift = (i - 2) * 3 - (i % 3) * 5 + rng.randint(-2, 2)
value = max(0, min(150, base + drift))
cells.append({"weekStart": (start + timedelta(weeks=i)).isoformat(), "utilization": value, "note": None})
return cells


def build_engineers(count: int = 30, seed: int = 42) -> list[dict]:
rng = random.Random(seed)
engineers: list[dict] = []
for index in range(count):
first, last = FIRST[index], LAST[index]
engineers = []
for i in range(count):
first, last = FIRST[i], LAST[i]
utilization = rng.randint(35, 115)
project_count = 0 if utilization < 55 else rng.randint(1, 3)
projects = rng.sample(PROJECT_IDS, k=min(project_count, len(PROJECT_IDS))) if project_count else []
assigned = 0 if utilization < 55 else rng.randint(1, 3)
projects = rng.sample(PROJECT_IDS, k=min(assigned, len(PROJECT_IDS))) if assigned else []
engineers.append(
{
"id": f"eng-{index + 1}",
"id": f"eng-{i + 1}",
"name": f"{first} {last}",
"role": pick_role(rng),
"role": _pick_role(rng),
"email": f"{first.lower()}.{last.lower()}@company.com",
"capacity": 100,
"utilization": utilization,
"status": status_for(utilization),
"status": _status_for(utilization),
"skills": rng.sample(SKILLS, k=rng.randint(2, 5)),
"currentProjects": projects,
"utilizationTimeline": build_timeline(utilization, rng),
"utilizationTimeline": _timeline(utilization, rng),
"avatar": None,
}
)
Expand Down
33 changes: 5 additions & 28 deletions apps/delta-command/backend/src/delta_command/json_db.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
from __future__ import annotations
"""Atomic JSON file store for the Delta Command database."""

import json
import os
import tempfile
from pathlib import Path

DEFAULT_DATA_PATH = Path(__file__).resolve().parents[2] / "config" / "data.json"
LEGACY_RUNTIME_PATH = Path(__file__).resolve().parents[2] / "config" / "runtime.json"


def database_path() -> Path:
override = os.environ.get("DELTA_DATA_PATH") or os.environ.get("DELTA_CONFIG_PATH")
if override:
return Path(override)
return DEFAULT_DATA_PATH
return Path(os.environ.get("DELTA_DATA_PATH") or DEFAULT_DATA_PATH)


def load_json(path: Path) -> dict:
if not path.exists():
return {}
text = path.read_text(encoding="utf-8").strip()
if not text:
return {}
return json.loads(text)
return json.loads(text) if text else {}


def save_json(path: Path, data: dict) -> None:
"""Write JSON atomically via a temp file in the same directory."""
path.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
fd, temp_name = tempfile.mkstemp(
dir=path.parent,
prefix=f".{path.stem}-",
suffix=".tmp",
)
fd, temp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.stem}-", suffix=".tmp")
temp_path = Path(temp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
Expand All @@ -43,17 +34,3 @@ def save_json(path: Path, data: dict) -> None:
finally:
if temp_path.exists():
temp_path.unlink(missing_ok=True)


def migrate_legacy_runtime(data_path: Path | None = None) -> None:
"""One-time migration: merge legacy runtime.json into the single data file."""
target = data_path or database_path()
legacy = LEGACY_RUNTIME_PATH
if not legacy.exists():
return
legacy_data = load_json(legacy)
if not legacy_data.get("engineers"):
legacy.unlink(missing_ok=True)
return
save_json(target, legacy_data)
legacy.unlink(missing_ok=True)
Loading