Skip to content

codriter/pulse-crm

Repository files navigation

ClientPulse CRM

A self-hosted B2B sales pipeline CRM — track deals, proposals, accounts, stakeholders, communications, and follow-ups in one place. Includes optional AI-assisted data entry via OpenRouter, OpenAI, or DeepSeek.


Architecture

pulse-crm/
├── main.py                  # ASGI entry point (imports app from app/main.py)
├── app/
│   ├── main.py              # FastAPI app, router registration, DB init, lead source seed
│   ├── config.py            # env-var config + session key management
│   ├── database.py          # SQLAlchemy engine + session (SQLite)
│   ├── templates_factory.py # Singleton Jinja2Templates with label globals registered
│   ├── models/              # ORM models (all imported in __init__.py for create_all)
│   │   ├── vacancy.py           # Deal (VacancyStatus, Priority enums)
│   │   ├── application.py       # Proposal (ApplicationStatus enum)
│   │   ├── application_note.py  # Proposal notes
│   │   ├── document.py          # Sales asset (DocumentType enum)
│   │   ├── contact.py           # Stakeholder
│   │   ├── communication.py     # Communication log (CommType enum)
│   │   ├── reminder.py          # Follow-up task (ReminderType enum)
│   │   ├── company.py           # Account
│   │   ├── company_note.py      # Account notes
│   │   ├── daily_routine.py     # Sales routine log
│   │   ├── fit_analysis.py      # AI fit/qualification analysis for a deal
│   │   ├── vacancy_site.py      # Lead source
│   │   └── setting.py           # AppSetting key/value store
│   ├── routers/             # FastAPI routers (one per resource)
│   │   ├── auth.py              # login / logout / registration
│   │   ├── dashboard.py
│   │   ├── vacancies.py         # Deals
│   │   ├── applications.py      # Proposals (scoped to a deal)
│   │   ├── documents.py         # Sales Assets
│   │   ├── contacts.py          # Stakeholders
│   │   ├── communications.py    # Communications (scoped to a deal)
│   │   ├── reminders.py         # Follow-ups
│   │   ├── companies.py         # Accounts
│   │   ├── daily_routine.py     # Sales Routine tracker
│   │   ├── for_processing.py    # Qualification Queue
│   │   ├── vacancy_sites.py     # Lead Sources
│   │   ├── io_router.py         # JSON import / export
│   │   ├── settings.py          # App settings
│   │   └── ai.py                # AI normalize + autofill endpoints
│   ├── services/
│   │   ├── stats.py             # Dashboard stats + overdue count
│   │   ├── document_rendering.py
│   │   ├── pdf_generation.py    # Playwright in-process PDF export
│   │   ├── settings_service.py  # get/set AppSetting helpers + defaults
│   │   ├── ai_service.py        # OpenRouter / OpenAI / DeepSeek API calls
│   │   ├── auth_service.py      # bcrypt password helpers
│   │   ├── labels.py            # Display label mappings for all enum types
│   │   └── io_schema.py         # FieldSpec definitions + serialize/deserialize helpers
│   ├── middleware/auth.py    # AuthMiddleware: enforces session auth on all non-/auth/* routes
│   ├── static/              # Shared CSS / JS assets
│   └── templates/           # Jinja2 HTML templates
│       ├── base.html            # Shared layout, sidebar nav
│       ├── macros.html          # Reusable form + badge macros
│       ├── pdf/                 # PDF HTML templates
│       └── <resource>/          # list, detail, form per resource
├── data/                    # SQLite DB + session key (git-ignored)
│   ├── crm.db
│   └── portfolio_demo_seed.json
├── scripts/
│   ├── seed_portfolio_demo.py   # Portfolio demo seed script
│   └── start.sh                 # Run tests then start server
└── pyproject.toml

Stack: Python 3.12 · FastAPI · SQLAlchemy (SQLite) · Jinja2 server-rendered templates · Tailwind CSS (CDN) · Playwright/Chromium (in-process PDF export) · python-markdown (server-side) · marked.js (live editor preview) · bcrypt + itsdangerous (session auth) · httpx (AI provider API calls)

DB: SQLite file at data/crm.db, path overridable via DB_PATH env var. Tables are auto-created on startup (Base.metadata.create_all). No migration framework — drop the file and restart to reset.

URL structure:

Prefix Resource
/ Redirects to Dashboard
/dashboard Dashboard
/vacancies Deals list / detail / form
/applications Proposals (scoped to a deal)
/documents Sales Assets library
/contacts Stakeholders list / detail / form
/communications Communications log (scoped to a deal)
/reminders Follow-ups list / form
/companies Accounts list / detail / form
/for-processing Qualification Queue
/vacancy-sites Lead Sources
/daily-routine Sales Routine tracker
/io Data Import / Export
/settings App settings (markdown mode, AI config, workflows)
/ai AI JSON API (normalize, autofill-company, models)

Running with Docker (recommended)

Prerequisites: Docker with Compose.

# build and start (first run pulls ~1.5 GB image — Chromium is bundled)
docker compose up -d --build

# view logs
docker compose logs -f

# stop
docker compose down

Open http://localhost:8002. On first run you will be prompted to register a password.

All data (crm.db, session.key) is stored in ./data/ on the host — it survives container restarts and rebuilds. To move the app to another machine, copy the project folder including data/.

By default the container runs the automated test suite before starting the server. To skip startup tests when needed:

RUN_TESTS_ON_START=0 docker compose up -d          # skip tests, no rebuild
RUN_TESTS_ON_START=0 docker compose up -d --build  # skip tests + rebuild image

Running Locally (without Docker)

Prerequisites: uv.

# install dependencies
uv sync

# install Chromium for PDF export (one-time, ~110 MB)
uv run playwright install chromium

# run tests, then start the server
bash scripts/start.sh

Open http://localhost:8000. On first run you will be prompted to register a password.

To use a custom DB path:

DB_PATH=/path/to/my.db uv run uvicorn main:app --reload

To override the PDF export timeout (default 30 s):

PDF_TIMEOUT_SECONDS=60 uv run uvicorn main:app --reload

To run in reload mode during development:

uv run pytest
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000

To skip tests in the startup script:

RUN_TESTS_ON_START=0 bash scripts/start.sh

Portfolio Demo Seed

To populate the app with realistic sample data for portfolio or demo purposes, run the seed script:

DB_PATH=data/crm.db uv run python scripts/seed_portfolio_demo.py

The script is idempotent — running it again without flags does nothing if demo data is already present. To wipe and re-seed:

DB_PATH=data/crm.db uv run python scripts/seed_portfolio_demo.py --force

The seed creates a representative set of accounts, stakeholders, deals (in various pipeline stages), proposals, communications, follow-ups, fit analyses, account notes, proposal notes, and daily routine logs. All dates are relative to today so the data always feels current.


Automated Tests

The project uses pytest with FastAPI's TestClient. Tests live in tests/ and use an isolated SQLite database at /tmp/job-crm-tests.db, so they do not touch data/crm.db.

# run the full test suite
uv run pytest

# run a single file
uv run pytest tests/test_documents.py

Current coverage:

  • Auth setup, login, invalid login, and protected route behavior
  • Sales Asset CRUD, Markdown rendering helpers, PDF eligibility, and functional PDF download
  • Deal creation, filtering, proposals, follow-up stats, dashboard rendering, and cascade delete
  • JSON import/export success and invalid JSON handling
  • AI normalize and autofill endpoints (guard logic, success paths, error handling, settings persistence)
  • Daily routine toggle, day rollover archiving, and log clearing
  • Settings: AI config, preview mode, daily routine steps, deal workflow steps

PDF tests assert functional behavior only: successful response, application/pdf, non-empty bytes, and download headers.


User Guide

Deals

The central entity. Every proposal, communication, and follow-up belongs to a deal.

  1. Add a deal — click Deals → + New Deal. Fill in the account, role/service title, lead source URL, work mode, budget range, and deadline.
  2. Set status — use the status dropdown on the deal detail page or inline from the list. Pipeline stages: New LeadQualifyProposal ReadyProposal SentFollow-up ScheduledNegotiationWon / Lost / Archived.
  3. PriorityLow / Medium / High, shown as a coloured badge.
  4. AI fill — paste a raw brief or job description and click Fill with AI to auto-populate fields (requires AI enabled in Settings).
  5. Deal workflow checklist — a per-deal step-by-step workflow (configurable in Settings). Checking the first step on a new deal snapshots the current global workflow steps onto that deal so its checklist is independent of future workflow edits.

Proposals

Each deal can have multiple proposals (e.g., revised offers after initial rejection).

  1. Open a deal and click New Proposal.
  2. Set the submission date, channel (LinkedIn / Email / Job Board / Direct / Referral / Agency), and status.
  3. Attach a proposal document and/or executive summary from your Sales Assets library using the type-in filter dropdowns. Click Create Proposal or Create Summary to create and pre-attach a new document in one step.

Accounts

Research profiles for the companies you are targeting.

  • Rich set of fields covering business overview, financials, tech stack, leadership, employee sentiment, and risk signals.
  • AI autofill — type an account name and click Autofill with AI to search the web and populate all fields automatically (uses Perplexity Sonar on OpenRouter by default, falls back to the selected model).
  • AI normalize — paste raw text (LinkedIn page, job post, etc.) and click Fill with AI to extract structured fields.
  • JSON import — paste a JSON blob into the form to bulk-fill all fields at once.
  • Search accounts by name, industry, or description from the list view.

Sales Assets

A Markdown-based library for proposals, executive summaries, and outreach messages.

  • Types: Master Proposal Template, Service-Line Proposal, Client-Specific Proposal, Executive Summary, Outreach Message, Follow-up Message
  • The editor supports Split View (edit + live preview side-by-side) and Preview Only mode.
  • Service-Line and Client-Specific Proposals can reference a Parent Template to track lineage.
  • Click Download PDF on eligible proposal and executive summary documents to download a formatted PDF.

Stakeholders

People you are in contact with or plan to reach out to.

  • Link stakeholders to one or more deals from the stakeholder detail page.
  • Relationship strength: Weak / Moderate / Strong.
  • Communications can optionally reference a stakeholder.
  • AI fill — paste raw contact info and click Fill with AI to auto-populate fields.

Communications

A log of every interaction for a deal (emails, LinkedIn messages, discovery call notes).

  • Types: Proposal Sent, LinkedIn Outreach, Email, Follow-up, Client Reply, Deal Lost, Meeting Scheduled, Discovery Completed, Internal Note
  • Fields: date, summary, full text, outcome, next step.

Follow-ups

Time-based tasks tied to a deal or stakeholder.

  • Types: Follow-up, Send Proposal, Meeting Prep, Send Sales Asset, Send Connection Request, Send First Message, Check Reply, Ask for Introduction, Reconnect Later, Other
  • Overdue follow-ups (due date in the past, not completed) are counted in the sidebar badge.
  • Mark a follow-up complete from the list or detail page.

Qualification Queue

A focused view of deals flagged for qualification review.

  • Mark any deal as Add to Queue from the deal detail page.
  • Set a processing date to schedule when to review the deal.
  • Work through queued deals to quickly decide whether to advance or drop each opportunity.

Lead Sources

A curated list of channels where you find and develop new business.

  • Categories: Owned Channel (your profiles and inbound), Prospecting (active outbound search), Partner / Marketplace (referral networks and agency channels).
  • Each source has a Priority (Critical / High / Medium / Low) and a custom display order within its category.
  • Pre-seeded with 12 sources including Upwork, LinkedIn Sales Navigator, Clutch, Contra, and a Referral Network entry.

Sales Routine

A daily habit tracker for pipeline-building activities.

  • The routine steps are configurable in Settings → Sales Routine.
  • Each day's progress is tracked separately. Completed steps are archived at midnight (on the first page visit after rollover) so you can review historical completion rates.
  • Click any step to toggle it done/undone for today.

Dashboard

Shows active deal counts by pipeline stage, conversion rates (Reply Rate, Negotiation Rate, Win Rate), overdue follow-ups, and a Sales Routine progress widget at a glance.

Settings

Accessible from the sidebar. Sections:

  1. Markdown Preview — set the default editor mode (editor or preview).
  2. AI Integration — enable/disable AI, connect providers, select the active model, and optionally override system prompts. See AI Integration.
  3. Sales Routine — edit the ordered list of daily steps (JSON array).
  4. Deal Workflow — edit the ordered checklist steps applied to new deals (JSON array).
  5. Data Management — view record counts per table and clear individual tables.

AI Integration

All AI features are opt-in and require at least one provider API key configured in Settings. The app supports three providers simultaneously — switch between them without re-entering keys.

Supported Providers

Provider Models Pricing shown in dropdown
OpenRouter Any model on the OpenRouter marketplace, including free models Live from API
OpenAI GPT-4o, GPT-4o mini, GPT-4 Turbo, GPT-3.5 Turbo, o1, o1-mini, o3-mini, and all versioned variants Hardcoded from published pricing
DeepSeek All DeepSeek chat models From API if available

Setup

  1. Go to Settings → AI Integration.
  2. Toggle Enable AI features.
  3. Enter API keys for whichever providers you want. Each key is saved independently.
  4. Click Connect for each provider to verify the key and load the model list.
  5. Select the active provider and model from the dropdowns. Token prices are shown where available.
  6. Save. AI buttons will appear on Account, Deal, and Stakeholder forms.

AI Features

Feature Where What it does
Normalize Deal form, Stakeholder form, Account form Paste raw text (brief, LinkedIn profile, etc.) → AI extracts and fills structured fields
Autofill Account form Type an account name → AI searches the web and populates all account fields

Prompt Customization

Both operations use default system prompts that can be overridden in Settings → AI Integration → Advanced. Leaving a field blank restores the built-in default.

  • Normalize prompt — controls how raw text is parsed into structured fields.
  • Autofill prompt — controls how the model searches and summarizes account information.

Model Selection Behaviour

  • Autofill on OpenRouter automatically uses perplexity/sonar (web-search capable) regardless of the selected model, then falls back to the selected model if Sonar fails. On OpenAI and DeepSeek it uses the selected model directly.
  • Normalize always uses the selected model.

API Endpoints

Method Path Description
GET /ai/models?provider=<provider>&api_key=<key> Fetch available models with pricing
POST /ai/normalize Extract structured fields from raw text
POST /ai/autofill-company Search the web and populate account fields

Normalize request body:

{ "entity_type": "vacancy", "raw_text": "Senior Engineer at Acme..." }

entity_type is one of vacancy, contact, company.

Autofill request body:

{ "query": "Stripe" }

Both endpoints return {"fields": {...}} on success or {"error": "..."} with an appropriate HTTP status on failure. AI must be enabled and an API key for the active provider must be saved in Settings, otherwise the endpoint returns 400.

OpenAI Model Pricing

Token prices are resolved from a built-in lookup table using the model ID prefix (USD per 1M tokens, input / output):

Model prefix Input Output
gpt-4o-mini $0.15 $0.60
gpt-4o $2.50 $10.00
gpt-4-turbo $10.00 $30.00
gpt-4 $30.00 $60.00
gpt-3.5-turbo $0.50 $1.50
o1 $15.00 $60.00
o1-mini $1.10 $4.40
o3-mini $1.10 $4.40

Versioned model IDs (e.g. gpt-4o-2024-11-20) inherit the price of their matching prefix. Non-chat models are filtered out of the dropdown entirely.


Database Access

The DB is a single SQLite file. To inspect or modify it directly:

# open an interactive SQL shell
sqlite3 data/crm.db

# useful queries
.tables
.schema vacancies
SELECT id, company, role_title, status FROM vacancies;
SELECT * FROM reminders WHERE completed = 0 AND due_date < date('now');

To back up the database:

cp data/crm.db data/crm-backup-$(date +%F).db

To reset (wipe all data):

rm data/crm.db
# restart the app — tables are recreated automatically

Import / Export

Go to Data Import / Export in the sidebar.

  • Export — downloads a crm-export-YYYY-MM-DD.json file with all accounts, deals, stakeholders, and sales assets.
  • Import — upload a JSON file in the same format. Unknown or invalid records are skipped silently; a count of imported records is shown after.

JSON Format

{
  "companies": [
    {
      "name": "Acme Corp",
      "website": "https://acme.com",
      "description": "B2B SaaS platform",
      "industry": "Software",
      "business_model": "SaaS",
      "target_customers": "Mid-market",
      "tech_stack": "Python, React",
      "headquarters": "Berlin, Germany",
      "company_size": "50-200",
      "notes": "Warm intro via referral"
    }
  ],
  "vacancies": [
    {
      "company": "Acme Corp",
      "role_title": "Backend Automation Sprint",
      "source": "Upwork",
      "url": "https://upwork.com/jobs/...",
      "location": "Remote",
      "work_mode": "Remote",
      "employment_type": "Contract",
      "salary_min": 5000,
      "salary_max": 8000,
      "salary_currency": "USD",
      "date_found": "2026-05-01",
      "deadline": "2026-05-15",
      "status": "Applied",
      "priority": "High",
      "vacancy_notes": "Strong Python focus, fast turnaround",
      "fit_notes": "Good match on automation experience",
      "applications": [
        {
          "application_date": "2026-05-05",
          "channel": "Direct",
          "positioning_angle": "Led similar migration at previous client",
          "notes": "Sent tailored proposal",
          "status": "Submitted"
        }
      ],
      "communications": [
        {
          "comm_date": "2026-05-07",
          "comm_type": "Reply Received",
          "summary": "Client asked for discovery call",
          "outcome": "Call booked for May 10",
          "next_step": "Prepare demo"
        }
      ],
      "reminders": [
        {
          "due_date": "2026-05-10",
          "task_type": "Interview Prep",
          "note": "Review client's existing stack",
          "completed": false
        }
      ]
    }
  ],
  "contacts": [
    {
      "full_name": "Jane Smith",
      "role_title": "Head of Engineering",
      "company": "Acme Corp",
      "linkedin_url": "https://linkedin.com/in/janesmith",
      "email": "jane@acme.com",
      "notes": "Decision maker, prefers async comms",
      "relationship_strength": "Moderate"
    }
  ],
  "documents": [
    {
      "title": "Master Proposal Template 2026",
      "doc_type": "Master CV",
      "content_markdown": "# Andrew Legovich\n\n## Services\n...",
      "notes": "Base template, update quarterly"
    }
  ]
}

Enum values reference (these are the raw values used in JSON — the UI shows commercial display labels):

Field Accepted values
vacancy.status New, To Review, Ready to Apply, Applied, Follow-Up Planned, Interviewing, Rejected, Offer, Closed / Archived
vacancy.priority Low, Medium, High
application.status Draft, Submitted, Acknowledged, In Review, Interview Scheduled, Interviewed, Rejected, Offer, Withdrawn
application.channel LinkedIn, Email, Job Board, Direct, Referral, Agency
communication.comm_type Application Submitted, LinkedIn Message, Email, Follow-up, Reply Received, Rejection, Interview Scheduled, Interview Completed, Internal Note
reminder.task_type Follow-up, Apply, Interview Prep, Send Documents, Send connection request, Send first message, Check reply, Ask for referral, Reconnect later, Other
document.doc_type Master CV, Role-specific CV, Tailored CV, Cover Letter, Outreach Message, Follow-up Message
contact.relationship_strength Weak, Moderate, Strong

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

Third-Party Dependencies

Library License
FastAPI MIT
SQLAlchemy MIT
Jinja2 BSD-3-Clause
Playwright Apache 2.0
httpx BSD-3-Clause
uvicorn BSD-3-Clause
python-markdown BSD-3-Clause
itsdangerous BSD-3-Clause
bcrypt Apache 2.0
python-multipart Apache 2.0
python-slugify MIT

All third-party components are subject to their respective licenses. This project optionally integrates with external AI services (OpenRouter, OpenAI, DeepSeek); usage of those services is subject to their own terms.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages