Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ HSTS_SECONDS=3600
HSTS_INCLUDE_SUBDOMAINS=False
HSTS_PRELOAD=False

# Rate limit for the public lead form, per client
PUBLIC_LEADS_RATE=20/hour

# Production only: console log level
LOG_LEVEL=INFO

Expand Down
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Report a problem or share an idea
about: Issues are welcome but may go unanswered; the project is not actively maintained.
---

<!--
lead-platform is complete and not actively maintained, so this issue may go unanswered.
If you need a fix, forking the repository is the quickest route; see CONTRIBUTING.md.
-->

## What happened, or what you'd like

## Steps to reproduce (for a problem)

1.
2.

## Where you ran it

<!-- For example: a local clone, the Docker image, docker compose, or your own deployment; your OS and browser. -->
21 changes: 21 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
lead-platform is complete and not actively maintained, so this pull request may not be reviewed.
Publishing your fork works just as well. Thank you for sharing!
-->

## What this changes

<!-- One or two sentences. -->

## If it changes the backend

- [ ] `flake8`
- [ ] `pytest`
- [ ] `python manage.py makemigrations --check --dry-run`
- [ ] `python manage.py spectacular --validate --fail-on-warn --file schema.yml`, with `frontend/src/types/api.ts` regenerated to match

## If it changes the frontend

- [ ] `pnpm lint`
- [ ] `pnpm test`
- [ ] `pnpm build`
84 changes: 83 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Expand Down Expand Up @@ -40,7 +44,7 @@ jobs:

- name: Check OpenAPI schema is up to date
run: |
python manage.py spectacular --validate --file "$RUNNER_TEMP/schema.yml"
python manage.py spectacular --validate --fail-on-warn --file "$RUNNER_TEMP/schema.yml"
diff -u schema.yml "$RUNNER_TEMP/schema.yml" || {
echo "::error file=backend/schema.yml::schema.yml is stale. Run 'python manage.py spectacular --file schema.yml' and commit it."
exit 1
Expand Down Expand Up @@ -115,6 +119,7 @@ jobs:
for i in $(seq 60); do curl -fsS -o /dev/null http://localhost:8000/ && break; sleep 1; done
curl -fsS http://localhost:8000/leads/7 | grep -q 'id="root"'
curl -fsS -o /dev/null http://localhost:8000/static/admin/css/base.css
curl -fsS -o /dev/null http://localhost:8000/static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui-bundle.js
test "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8000/api/leads/)" = 401
curl -fsS -X POST http://localhost:8000/api/auth/login/ \
-H 'Content-Type: application/json' \
Expand All @@ -123,3 +128,80 @@ jobs:
- name: Container logs
if: always()
run: docker logs app || true

# ----------------------------------------------------------------------------
# Releases: pushing a v* tag publishes the image and the GitHub Release, only
# after every check above has passed
# ----------------------------------------------------------------------------
publish:
name: Publish image
if: startsWith(github.ref, 'refs/tags/v')
needs: [backend, frontend, docker]
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7

- name: Tag matches the package version
run: |
version=$(node -p 'require("./frontend/package.json").version')
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match version $version in frontend/package.json"
exit 1
fi

- uses: docker/setup-qemu-action@v4.4.0
- uses: docker/setup-buildx-action@v4.4.1
- uses: docker/login-action@v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image name
id: image
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

- uses: docker/build-push-action@v7.4.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ steps.image.outputs.name }}:${{ github.ref_name }}
${{ steps.image.outputs.name }}:latest
labels: |
org.opencontainers.image.version=${{ github.ref_name }}
org.opencontainers.image.revision=${{ github.sha }}
# The package page reads these from the multi-platform index
annotations: |
index:org.opencontainers.image.description=Lead management API and web app: JWT auth, roles, a validated status pipeline, notes and an automatic activity timeline. Django REST Framework + React.
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
index:org.opencontainers.image.licenses=MIT

release:
name: GitHub Release
needs: publish
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v7

- name: Release assets
run: |
mkdir dist
cp backend/schema.yml dist/schema.yml
(cd dist && sha256sum -- * > SHA256SUMS && cat SHA256SUMS)

- name: Release notes from CHANGELOG.md
run: |
awk -v heading="## ${GITHUB_REF_NAME#v} " 'index($0, heading) == 1 {found=1; next} /^## / {found=0} found' CHANGELOG.md > notes.md
test -s notes.md || { echo "::error::No CHANGELOG.md entry for ${GITHUB_REF_NAME#v}"; exit 1; }

- name: Create the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" dist/* --verify-tag --title "lead-platform $GITHUB_REF_NAME" --notes-file notes.md
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## 1.0.0 (2026-09-26)

The first and final planned release. lead-platform is complete as of this version and not actively maintained; fork it freely.

- **Leads API:** create, list, retrieve, edit and delete leads. Filter by status, assignee, creator and company; search across name, email, phone and company; sort; and paginate (20 per page, up to 100). OpenAPI 3 docs are at `/api/docs/` and `/api/redoc/`.
- **Status pipeline:** New → Contacted → Qualified → Proposal → Won, and Lost from any open stage. Transitions are enforced by one service function, and a blocked change returns `400` with the reason.
- **Activity timeline:** creating, editing, status changes, assignment and notes are recorded automatically, in the same transaction as the change.
- **Roles:** Admins see and manage every lead; Members see and work on the leads they created or are assigned to. Only Admins delete or assign. Notes are edited or deleted by their author or an Admin. A lead a member can't see returns `404`.
- **Authentication:** JWTs with 15-minute access tokens and 7-day refresh tokens that rotate and are blacklisted after use. Includes logout, the current user, and the list of assignable members.
- **Public lead form:** an unauthenticated endpoint for a website enquiry form, rate-limited per client (`PUBLIC_LEADS_RATE`, default 20 an hour).
- **Web app:** React 19 and TypeScript, with types generated from the API schema. Sign-in with a shared, single-flight token refresh; a dashboard; a searchable, filterable and paginated lead list; and a lead page with status, assignment, notes and the timeline.
- **Quality:** 99 backend tests and 74 frontend tests. CI runs flake8, a missing-migrations check, a stale-schema and stale-types check, ESLint, a production build, and a Docker build with a smoke test.
- **Deployment:** one Docker image (amd64 and arm64) serves the API, Django admin and the web app with gunicorn and WhiteNoise. It applies migrations and creates the first admin on start. There is a Render blueprint for a free demo on Neon Postgres, and a `docker compose` setup for local Postgres.

Release assets: `schema.yml` (the OpenAPI schema) and `SHA256SUMS`. Container image: `ghcr.io/code-kasha/lead-platform:v1.0.0` (also `latest`).
4 changes: 2 additions & 2 deletions CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
- [x] Consistent Docstring
- [x] Import Cleanup
- [x] Query Optimization
- [x] 80 Automated Tests Passing
- [x] 99 Automated Tests Passing

---

Expand Down Expand Up @@ -178,7 +178,7 @@
- [x] Frontend Build (`pnpm build`)
- [x] Frontend Lint (`pnpm lint`)
- [x] Frontend Tests (74/74, `pnpm test`)
- [x] Backend Tests (80/80)
- [x] Backend Tests (99/99)
- [x] Manual End-to-End Testing
- [x] Swagger Verification

Expand Down
22 changes: 17 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Full-stack Lead Management Platform: Django REST Framework API + React/TypeScrip
Covers authentication, lead capture, assignment, a validated status lifecycle, notes, and
an automatic activity timeline.

**Status: complete as of v1.0.0 and not actively maintained** (see `CONTRIBUTING.md`). Releases
are cut by tagging; see "Releases" below.

Repo: `github.com/code-kasha/lead-platform` · Author: Akash Damle

## Layout
Expand All @@ -27,7 +30,7 @@ frontend/
layouts/ page shells
pages/ route-level views
routes/ routing and guards
docs/ architecture, assessments, DEPLOY.md
docs/ api.md (API reference), DEPLOY.md, architecture and assessments
Dockerfile one image: builds the frontend, serves API + admin + SPA (gunicorn + WhiteNoise)
render.yaml Render blueprint (free Docker web service; database on Neon)
screenshots/ UI captures referenced by the README
Expand Down Expand Up @@ -75,7 +78,7 @@ CI (`.github/workflows/ci.yml`) runs on PRs and pushes to `main`: flake8, a miss
check, a stale-schema check (`backend/schema.yml` must match `manage.py spectacular`), pytest,
a stale-types check (`frontend/src/types/api.ts` must match `openapi-typescript` output), then
`pnpm lint`, `pnpm test` and `pnpm build`. A third job builds the Docker image and smoke-tests the
running container (SPA route, admin static, API 401, admin login). After changing views or serializers, regenerate both files:
running container (SPA route, admin and Swagger UI static, API 401, admin login). After changing views or serializers, regenerate both files:
`python manage.py spectacular --file schema.yml` (from `backend/`) and
`pnpm exec openapi-typescript ../backend/schema.yml -o src/types/api.ts` (from `frontend/`).
Python lint config is `.flake8` (max line length 120).
Expand All @@ -87,7 +90,15 @@ to `backend/frontend_dist/`; Django serves `index.html` for every path outside `
`/admin/` and `/static/` (`apps/common/views.py#frontend_index`, last route in
`config/urls.py`), and WhiteNoise serves the assets. `docker-entrypoint.sh` runs `migrate`
and `ensure_superuser` (idempotent) on every start. `docker compose up --build` runs it
locally against Postgres.
locally against Postgres. Swagger UI and ReDoc are served from `drf-spectacular-sidecar`
(`SWAGGER_UI_DIST`/`REDOC_DIST = "SIDECAR"`), not a CDN.

## Releases

Pushing a `v*` tag runs CI plus `publish` (checks the tag matches `version` in
`frontend/package.json`, then pushes a multi-arch image to `ghcr.io/code-kasha/lead-platform`
with the version tag and `latest`) and `release` (a GitHub Release whose notes are the matching
`## X.Y.Z (date)` entry in `CHANGELOG.md`, with `schema.yml` and `SHA256SUMS` attached).

## Conventions

Expand Down Expand Up @@ -122,5 +133,6 @@ locally against Postgres.

## Known issues

- The backend requires **Python ≥ 3.12** (Django 6.0). Tests need `SECRET_KEY` and
`DATABASE_URL` set (e.g. `DATABASE_URL=sqlite:///:memory:`); the full suite is 80 tests.
- The backend requires **Python ≥ 3.12** (Django 6.1). Tests need `SECRET_KEY` and
`DATABASE_URL` set (e.g. `DATABASE_URL=sqlite:///:memory:`); the full suite is 99 tests
(frontend: 74).
48 changes: 48 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Contributing to lead-platform

Thanks for looking. A quick word on where things stand, then how to work on the code.

## The project is finished

As of v1.0.0, lead-platform is complete and is not actively maintained. It works as-is, and no further updates are planned. Issues and pull requests are welcome, but they may go unanswered. Please don't wait on a reply: fork it, reuse it and grow it in whatever direction you need.

## License

The code is under the [MIT License](LICENSE), with no extra conditions. Use it, change it, host it or sell it. The license only asks that its notice stays with copies of the code.

A mention of lead-platform in your project is appreciated, never required.

## Changing the code

The API comes first. The backend owns the contract, and the frontend uses TypeScript types generated from it. The code keeps these rules (details in [`CLAUDE.md`](CLAUDE.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)):

- **Views handle requests and responses only.** Business logic goes in `apps/*/services.py`: `Request → View → Serializer → Service → Database`.
- **Service functions are transactional**, and they are the only code that writes activity records. A change and its timeline entry are saved together, or neither is.
- **Status only changes through `change_lead_status`**, which enforces the transitions in `apps/leads/constants.py`.
- **Never hand-write API types in the frontend.** After changing a view or serializer, regenerate the schema and the types:

```sh
cd backend && python manage.py spectacular --validate --fail-on-warn --file schema.yml
cd ../frontend && pnpm exec openapi-typescript ../backend/schema.yml -o src/types/api.ts
```

Use Python 3.12 or later, Node.js 22 and [pnpm](https://pnpm.io/). These are the checks CI runs on every push and pull request, along with a Docker build and smoke test:

```sh
# backend/ (tests need SECRET_KEY and DATABASE_URL, e.g. sqlite:///:memory:)
pip install -r requirements.txt
flake8
python manage.py makemigrations --check --dry-run
python manage.py spectacular --validate --fail-on-warn --file schema.yml
pytest

# frontend/
pnpm install --frozen-lockfile
pnpm lint
pnpm test
pnpm build
```

The 99 backend tests cover authentication, token rotation and blacklisting, role permissions, lead visibility for each role, create and edit with the activities they record, every allowed and blocked status change, assignment, notes and their owners, filtering, search, ordering, pagination, the public form and its rate limit, the schema, and the production settings. The 74 frontend tests cover the auth guard and the shared token refresh, the login flow, the dashboard, the lead list and its query parameters, and the lead page's status, assignment and notes against a fake API that enforces the backend's status rules.

To release a fork, set `version` in `frontend/package.json`, add a `CHANGELOG.md` entry and push a `v*` tag. CI then publishes the container image to the GitHub Container Registry and creates the GitHub Release. See [`docs/DEPLOY.md`](docs/DEPLOY.md#ci-and-releases).
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
# React app (same origin, so the frontend calls /api directly).

# ------------------------------------------------------------------------------
# Stage 1: build the frontend
# Stage 1: build the frontend (static files, so it runs on the build machine's
# own platform even when the image is built for another one)
# ------------------------------------------------------------------------------
FROM node:22-slim AS frontend
FROM --platform=$BUILDPLATFORM node:22-slim AS frontend

WORKDIR /app/frontend

Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Akash Damle

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading