init local package repo

This commit is contained in:
MacPro
2026-02-27 12:44:50 -06:00
commit 7027efd777
34 changed files with 20093 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Copy this file to .env.beta before running ./start_beta.sh
# Required for production mode
CRM_SECRET_KEY=replace-with-a-long-random-secret
# Optional overrides
# CRM_CORS_ORIGIN=http://100.x.y.z:8080
# CRM_LOGIN_RATE_LIMIT_PER_MIN=20
# CRM_WRITE_RATE_LIMIT_PER_MIN=300
# CRM_PORT=8080
+28
View File
@@ -0,0 +1,28 @@
# Remote Private Beta (Tailscale)
## 1) One-time prep on host laptop
1. Install and connect Tailscale.
2. In this project folder, create beta env:
- `cp .env.beta.example .env.beta`
3. Set a strong secret in `.env.beta` for `CRM_SECRET_KEY`.
## 2) Start beta server
- `./start_beta.sh`
- Optional custom port: `./start_beta.sh 8080`
The script prints the Tailscale URL if Tailscale is running.
## 3) Invite users
- Log in as admin.
- Settings -> Admin -> Invite User.
- Share the Tailscale URL and credentials with each tester.
## 4) Pre-flight safety checks before each test session
1. Run backup now in Settings -> Admin.
2. Run backup verification in Settings -> Admin -> Reliability Checks.
3. Confirm Security panel has no secret warning.
## 5) Troubleshooting
- If users cannot connect, confirm host shows `tailscale status` as online.
- If CORS error appears, set exact origin in `.env.beta` as `CRM_CORS_ORIGIN=http://<tailscale-ip>:8080` and restart.
- If port is in use, run `./start_beta.sh 8090`.
+92
View File
@@ -0,0 +1,92 @@
# Venture CRM Roadmap (Airtable Replacement)
## Current status
- Premium Airtable-like frontend grid exists and is actively iterating.
- Backend now has production-grade APIs for:
- `GET /api/fundraising/state`
- `PUT /api/fundraising/state` (with optimistic version check)
- `GET /api/fundraising/export`
- `POST /api/fundraising/backup`
- `POST /api/fundraising/restore-preview`
- `POST /api/fundraising/restore`
- `GET /api/fundraising/backups`
- `GET/PATCH /api/fundraising/backup-policy`
- `GET /api/fundraising/relational-summary`
- `GET /api/feature-requests`
- `POST /api/feature-requests`
- `PATCH /api/feature-requests/:id`
- New DB tables:
- `fundraising_state`
- `fundraising_investors`
- `fundraising_contacts`
- `fundraising_funds`
- `fundraising_commitments`
- `fundraising_views`
- `feature_requests`
- `app_settings`
- Grid saves/restores now sync into relational fundraising tables automatically.
- Formula engine is now sandboxed (no `eval`/`new Function`) with expanded function support.
- Automation engine v1 added:
- Rule table + toggle API
- List memberships (`main`, `follow_up`, `graveyard`, `longshot`, `all`)
- Automation run log
- Collaboration/reliability additions:
- Unified activity feed API (`audit` + `automation` + `backup`)
- Backup integrity verification API
- Better version-conflict metadata (`updated_at`, `updated_by`)
- Security hardening additions:
- Basic IP rate limiting (login and write APIs)
- Configurable CORS origin (`CRM_CORS_ORIGIN`)
- Production secret enforcement (`CRM_ENV=production` requires `CRM_SECRET_KEY`)
- Security status API + go-live checklist (`SECURITY.md`)
## Phase 1 (Production foundation)
1. Persist grid + views on backend
- Wire frontend fundraising grid reads/writes to `/api/fundraising/state`.
- Keep localStorage only as emergency fallback.
- Add autosave debounce and conflict handling (`expected_version`).
2. Admin-invite auth model
- Disable self-register for non-admin users.
- Add admin-only invite/create-user endpoint.
- Keep role model: `admin`, `member`.
3. Deployment and remote access
- Add `docker-compose` for one-command launch.
- Reverse proxy + TLS option (Caddy/Traefik) for non-Tailscale deployments.
- Recommended for your use case: Tailscale private access to laptop host.
4. Data safety and operations
- Automated nightly SQLite backups and restore test script.
- Add `/api/fundraising/export` for JSON snapshot export.
- Add health/readiness checks.
## Phase 2 (Airtable parity)
1. Advanced views
- Multi-condition filter groups (AND/OR groups)
- Multi-column sorting
- Pinned/frozen columns
- Personal vs shared views
2. Formula engine v2
- Add functions: `SUM`, `MIN`, `MAX`, `ROUND`, `ABS`, `CONCAT` (done)
- Type-aware formulas and better errors
- Dependency graph and recalculation rules
3. Activity + audit
- Record-level change history in UI
- Last modified by / at fields
- Restore archived rows
## Phase 3 (Team workflow and automation)
1. Tasks/reminders tied to investors/contacts
2. Automation rules (graveyard/follow-up triggers)
3. Email/communication integrations (optional)
4. Granular permissions (if team grows)
## Definition of done for "Airtable substitute" v1
- Team can manage all investors in one master table
- Saved views replicate current Airtable workflows
- CSV import from Airtable is reliable and repeatable
- Data persists safely and supports multi-user access
- Auth is invite-only and backups are automated
+32
View File
@@ -0,0 +1,32 @@
# Venture CRM Go-Live Security Checklist
## 1) Secrets and environment
- Set `CRM_ENV=production`.
- Set a strong `CRM_SECRET_KEY` (required in production mode).
- Set `CRM_CORS_ORIGIN` to your exact app origin (not `*`).
- Optional rate limits:
- `CRM_LOGIN_RATE_LIMIT_PER_MIN` (default `20`)
- `CRM_WRITE_RATE_LIMIT_PER_MIN` (default `300`)
## 2) Network access
- Preferred: Tailscale private access.
- Run app on local host machine; share via tailnet only.
- Restrict OS firewall to Tailscale interface where possible.
## 3) TLS/HTTPS
- If app is exposed beyond tailnet, place behind HTTPS reverse proxy (Caddy/Nginx/Traefik).
- Do not expose raw HTTP directly to the internet.
## 4) Accounts and auth
- Keep invite-only user creation through admin settings.
- Rotate temporary passwords after onboarding.
- Disable/deactivate stale users.
## 5) Backups and restore safety
- Keep scheduled backups enabled.
- Run backup verification after major updates.
- Test restore in a non-primary copy before production restore.
## 6) Operational monitoring
- Review activity feed and audit log regularly.
- Watch `429` responses as early abuse/misconfiguration signal.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
fastapi==0.109.2
uvicorn[standard]==0.27.1
sqlalchemy==2.0.27
alembic==1.13.1
pydantic==2.6.1
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.9
aiofiles==23.2.1
httpx==0.27.0
+3969
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,280 @@
{
"backup_at": "2026-02-26T22:42:42.728148Z",
"version": 1,
"updated_at": "2026-02-26 22:42:42",
"grid": {
"columns": [
{
"id": "investor_name",
"label": "Investor Name",
"type": "text",
"width": 220
},
{
"id": "contacts",
"label": "Contacts",
"type": "contacts",
"width": 260
},
{
"id": "notes",
"label": "Notes / Communication / Outreach",
"type": "longtext",
"width": 420
},
{
"id": "priority",
"label": "Priority",
"type": "checkbox",
"width": 110
},
{
"id": "follow_up",
"label": "Follow up",
"type": "checkbox",
"width": 110
},
{
"id": "lead",
"label": "Lead",
"type": "select",
"options": [
"JK",
"Grant",
"MB",
"Parker",
"Other"
],
"width": 130
},
{
"id": "graveyard",
"label": "Graveyard",
"type": "checkbox",
"width": 115
},
{
"id": "longshot_followup",
"label": "Longshot Followup",
"type": "checkbox",
"width": 155
},
{
"id": "fund_i",
"label": "Fund I",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "fund_ii",
"label": "Fund II",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "fund_iii",
"label": "Fund III",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "tactical_fund",
"label": "Tactical Fund",
"type": "currency",
"isFund": true,
"width": 140
},
{
"id": "pawn_to_e4",
"label": "Pawn to E4",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "ten31_terahash",
"label": "Ten31 Terahash",
"type": "currency",
"isFund": true,
"width": 150
},
{
"id": "sats_and_stats",
"label": "Sats and Stats",
"type": "currency",
"isFund": true,
"width": 140
},
{
"id": "pawn_to_f4",
"label": "Pawn to f4",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "join_the_fold",
"label": "Join the Fold",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "total_invested",
"label": "Total invested",
"type": "rollup",
"readOnly": true,
"width": 150
},
{
"id": "tactical_fund_commit_date",
"label": "Tactical Fund Commit Date",
"type": "date",
"width": 180
}
],
"rows": [
{
"id": "inv-1",
"investor_name": "Caprock / Grey Street",
"contacts": [
{
"name": "Jeffrey Friedstein",
"email": "jeffrey@example.com",
"title": "",
"city": "New York City",
"state": "NY",
"country": "USA",
"location_query": "New York City"
},
{
"name": "Jay P",
"email": "jay@example.com",
"title": "Analyst",
"city": "",
"state": "",
"country": "",
"location_query": ""
}
],
"notes": "Intro from Alan Handler. Potentially interested in Strike.",
"priority": true,
"follow_up": true,
"lead": "JK",
"graveyard": false,
"longshot_followup": false,
"fund_i": 0,
"fund_ii": 2500000,
"fund_iii": 0,
"tactical_fund": 0,
"pawn_to_e4": 0,
"ten31_terahash": 0,
"sats_and_stats": 0,
"pawn_to_f4": 0,
"join_the_fold": 0,
"tactical_fund_commit_date": ""
},
{
"id": "inv-2",
"investor_name": "Comer Family Office",
"contacts": [
{
"name": "Michael O'Shaughnessy",
"email": "mike@example.com",
"title": "",
"city": "Austin",
"state": "TX",
"country": "USA",
"location_query": "Austin"
}
],
"notes": "Met in Austin. Wants updates in Q2.",
"priority": false,
"follow_up": true,
"lead": "Grant",
"graveyard": false,
"longshot_followup": true,
"fund_i": 0,
"fund_ii": 1000000,
"fund_iii": 0,
"tactical_fund": 500000,
"pawn_to_e4": 0,
"ten31_terahash": 0,
"sats_and_stats": 0,
"pawn_to_f4": 0,
"join_the_fold": 0,
"tactical_fund_commit_date": "2026-03-15"
}
]
},
"views": [
{
"id": "view-main",
"name": "Main Fundraising",
"filters": {
"includeGraveyard": false,
"followUpOnly": false,
"longshotOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-followup",
"name": "Follow-up List",
"filters": {
"includeGraveyard": false,
"followUpOnly": true,
"longshotOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-longshot",
"name": "Longshot Followup",
"filters": {
"includeGraveyard": false,
"followUpOnly": false,
"longshotOnly": true,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-graveyard",
"name": "Graveyard",
"filters": {
"includeGraveyard": true,
"followUpOnly": false,
"longshotOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-all",
"name": "All Investors",
"filters": {
"includeGraveyard": true,
"followUpOnly": false,
"longshotOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
}
]
}
@@ -0,0 +1,210 @@
{
"backup_at": "2026-02-27T16:42:14.902533Z",
"version": 1,
"updated_at": "2026-02-27 16:42:14",
"grid": {
"columns": [
{
"id": "investor_name",
"label": "Investor Name",
"type": "text",
"width": 220
},
{
"id": "contacts",
"label": "Contacts",
"type": "contacts",
"width": 260
},
{
"id": "log_action",
"label": "Log",
"type": "action",
"readOnly": true,
"width": 90
},
{
"id": "notes",
"label": "Notes / Communication / Outreach",
"type": "longtext",
"width": 420
},
{
"id": "notes_last_modified",
"label": "Notes Last Modified",
"type": "date",
"readOnly": true,
"width": 180
},
{
"id": "last_communication_date",
"label": "Last Communication Date",
"type": "date",
"readOnly": true,
"width": 195
},
{
"id": "priority",
"label": "Priority",
"type": "checkbox",
"width": 110
},
{
"id": "follow_up",
"label": "Follow up",
"type": "checkbox",
"width": 110
},
{
"id": "lead",
"label": "Lead",
"type": "select",
"options": [
"JK",
"Grant",
"MB",
"Parker",
"Other"
],
"width": 130
},
{
"id": "graveyard",
"label": "Graveyard",
"type": "checkbox",
"width": 115
},
{
"id": "fund_i",
"label": "Fund I",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "fund_ii",
"label": "Fund II",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "fund_iii",
"label": "Fund III",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "tactical_fund",
"label": "Tactical Fund",
"type": "currency",
"isFund": true,
"width": 140
},
{
"id": "pawn_to_e4",
"label": "Pawn to E4",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "ten31_terahash",
"label": "Ten31 Terahash",
"type": "currency",
"isFund": true,
"width": 150
},
{
"id": "sats_and_stats",
"label": "Sats and Stats",
"type": "currency",
"isFund": true,
"width": 140
},
{
"id": "pawn_to_f4",
"label": "Pawn to f4",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "join_the_fold",
"label": "Join the Fold",
"type": "currency",
"isFund": true,
"width": 130
},
{
"id": "total_invested",
"label": "Total invested",
"type": "rollup",
"readOnly": true,
"width": 150
},
{
"id": "tactical_fund_commit_date",
"label": "Tactical Fund Commit Date",
"type": "date",
"width": 180
}
],
"rows": []
},
"views": [
{
"id": "view-main",
"name": "Main Fundraising",
"filters": {
"includeGraveyard": false,
"graveyardOnly": false,
"followUpOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-followup",
"name": "Follow-up List",
"filters": {
"includeGraveyard": false,
"graveyardOnly": false,
"followUpOnly": true,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-graveyard",
"name": "Graveyard",
"filters": {
"includeGraveyard": true,
"graveyardOnly": true,
"followUpOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
},
{
"id": "view-all",
"name": "All Investors",
"filters": {
"includeGraveyard": true,
"graveyardOnly": false,
"followUpOnly": false,
"lead": ""
},
"quickSearch": "",
"hiddenColumns": [],
"columnFilters": []
}
]
}
BIN
View File
Binary file not shown.
View File
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Ten31">
<rect x="2" y="2" width="60" height="60" rx="8" fill="#0b1118" stroke="#ffffff" stroke-width="2"/>
<text x="32" y="41" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="700" font-family="Georgia, 'Times New Roman', serif">T31</text>
</svg>

After

Width:  |  Height:  |  Size: 388 B

+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 722.69 280.85">
<defs>
<style>
.cls-1 {
font-family: LTCGoudyOldstylePro-Bold, 'LTC Goudy Oldstyle Pro';
font-size: 192px;
font-weight: 700;
}
.cls-1, .cls-2, .cls-3 {
fill: #fff;
}
.cls-2, .cls-4 {
stroke-width: 3px;
}
.cls-2, .cls-4, .cls-3 {
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-4 {
fill: none;
}
.cls-5 {
letter-spacing: -.06em;
}
</style>
</defs>
<text class="cls-1" transform="translate(120.54 208.45)"><tspan class="cls-5" x="0" y="0">T</tspan><tspan x="120.96" y="0">en31</tspan></text>
<g>
<polygon class="cls-3" points="95.52 140.42 54.54 154.4 54.54 126.45 95.52 140.42"/>
<line class="cls-2" x1="0" y1="140.42" x2="60.54" y2="140.42"/>
</g>
<rect class="cls-4" x="97.1" y="1.5" width="527.95" height="277.85"/>
<g>
<polygon class="cls-3" points="721.15 140.42 680.16 154.4 680.16 126.45 721.15 140.42"/>
<line class="cls-2" x1="625.62" y1="140.42" x2="686.16" y2="140.42"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+7485
View File
File diff suppressed because it is too large Load Diff
+3571
View File
File diff suppressed because it is too large Load Diff
+3559
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# Venture Fund CRM — Database Backup Script
# ═══════════════════════════════════════════════════════════════
#
# Usage:
# ./scripts/backup.sh # Backup to ./backups/
# ./scripts/backup.sh /path/to/backups # Backup to custom dir
#
# Automate with cron:
# crontab -e
# 0 2 * * * /path/to/venture-crm/scripts/backup.sh >> /var/log/crm-backup.log 2>&1
#
# ═══════════════════════════════════════════════════════════════
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
DB_PATH="$PROJECT_DIR/data/crm.db"
BACKUP_DIR="${1:-$PROJECT_DIR/backups}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/crm_backup_$TIMESTAMP.db"
# Create backup directory
mkdir -p "$BACKUP_DIR"
if [ ! -f "$DB_PATH" ]; then
echo "ERROR: Database not found at $DB_PATH"
exit 1
fi
# Use SQLite's backup command for a safe, consistent backup
# This works even if the server is running
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
# Get file size
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup created: $BACKUP_FILE ($SIZE)"
# Clean up old backups (keep last 30)
BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/crm_backup_*.db 2>/dev/null | wc -l)
if [ "$BACKUP_COUNT" -gt 30 ]; then
REMOVE_COUNT=$((BACKUP_COUNT - 30))
ls -1t "$BACKUP_DIR"/crm_backup_*.db | tail -n "$REMOVE_COUNT" | xargs rm -f
echo " Cleaned up $REMOVE_COUNT old backup(s). Keeping last 30."
fi
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Create a new user for the Venture Fund CRM."""
import sys
import os
import getpass
# Add parent directory to path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'backend'))
from server import get_db, hash_password, generate_id, init_db
def main():
init_db()
print("\n Create New CRM User")
print(" " + "=" * 30 + "\n")
username = input(" Username: ").strip()
if not username:
print(" Error: Username required")
sys.exit(1)
email = input(" Email: ").strip()
if not email:
print(" Error: Email required")
sys.exit(1)
full_name = input(" Full Name: ").strip()
if not full_name:
print(" Error: Full name required")
sys.exit(1)
password = getpass.getpass(" Password: ")
if len(password) < 6:
print(" Error: Password must be at least 6 characters")
sys.exit(1)
confirm = getpass.getpass(" Confirm Password: ")
if password != confirm:
print(" Error: Passwords don't match")
sys.exit(1)
role = input(" Role (admin/manager/member) [member]: ").strip() or "member"
if role not in ['admin', 'manager', 'member']:
print(" Error: Invalid role")
sys.exit(1)
conn = get_db()
# Check for existing
existing = conn.execute("SELECT id FROM users WHERE username = ? OR email = ?",
(username, email)).fetchone()
if existing:
print(f"\n Error: User with that username or email already exists")
conn.close()
sys.exit(1)
user_id = generate_id()
conn.execute("""
INSERT INTO users (id, username, email, password_hash, full_name, role)
VALUES (?, ?, ?, ?, ?, ?)
""", (user_id, username, email, hash_password(password), full_name, role))
conn.commit()
conn.close()
print(f"\n User '{username}' created successfully!")
print(f" Role: {role}")
print(f" ID: {user_id}\n")
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Reset a user's password in the Venture Fund CRM."""
import sys
import os
import getpass
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'backend'))
from server import get_db, hash_password
def main():
print("\n Reset User Password")
print(" " + "=" * 30 + "\n")
username = input(" Username: ").strip()
if not username:
print(" Error: Username required")
sys.exit(1)
conn = get_db()
user = conn.execute("SELECT id, full_name FROM users WHERE username = ?", (username,)).fetchone()
if not user:
print(f" Error: User '{username}' not found")
conn.close()
sys.exit(1)
print(f" Found user: {user['full_name']}")
password = getpass.getpass(" New Password: ")
if len(password) < 6:
print(" Error: Password must be at least 6 characters")
sys.exit(1)
confirm = getpass.getpass(" Confirm Password: ")
if password != confirm:
print(" Error: Passwords don't match")
sys.exit(1)
conn.execute("UPDATE users SET password_hash = ? WHERE id = ?",
(hash_password(password), user['id']))
conn.commit()
conn.close()
print(f"\n Password reset successfully for '{username}'!\n")
if __name__ == "__main__":
main()
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# Venture Fund CRM — Start Script
# ═══════════════════════════════════════════════════════════════
#
# Usage:
# ./start.sh # Start on default port 8080
# ./start.sh 3000 # Start on custom port
# CRM_HOST=0.0.0.0 ./start.sh # Bind to all interfaces (for LAN access)
#
# ═══════════════════════════════════════════════════════════════
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PORT="${1:-${CRM_PORT:-8080}}"
export CRM_PORT="$PORT"
export CRM_HOST="${CRM_HOST:-0.0.0.0}"
echo ""
echo " ╔══════════════════════════════════════════╗"
echo " ║ Venture Fund CRM ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
echo " Starting server on port $PORT..."
echo " Local: http://localhost:$PORT"
echo " Network: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'your-ip'):$PORT"
echo ""
echo " Press Ctrl+C to stop"
echo ""
cd "$SCRIPT_DIR"
python3 backend/server.py
+33
View File
@@ -0,0 +1,33 @@
# Deploy on StartOS 0.3.5 (Raspberry Pi)
## 1) Build the package on your Mac
```bash
cd /Users/macpro/Projects/CRM
make -C start9/0.3.5 package
```
This creates:
- `start9/0.3.5/image.tar`
- `start9/0.3.5/ten31-database.s9pk`
## 2) Upload package to StartOS
1. Open StartOS web UI.
2. Go to Services -> Sideload Package (or equivalent 0.3.5 menu).
3. Upload `ten31-database.s9pk`.
4. Install and start the service.
## 3) First run
1. Open the service UI.
2. Create first admin account on the login screen.
3. In Settings, run one manual backup immediately.
## 4) Data persistence contract
- App DB path: `/data/crm.db`
- Backup path: `/data/backups`
Because these are in the persistent service volume, app restarts/upgrades do not erase data.
## 5) Before any upgrade/migration
1. Run manual backup in-app.
2. Export fundraising state in-app.
3. Keep both files off-device as recovery copy.
+25
View File
@@ -0,0 +1,25 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
CRM_ENV=production \
CRM_HOST=0.0.0.0 \
CRM_PORT=8080 \
CRM_DATA_DIR=/data \
CRM_FRONTEND_DIR=/app/frontend
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
COPY backend/server.py /app/backend/server.py
COPY frontend /app/frontend
COPY start9/0.3.5/docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh
COPY start9/0.3.5/healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/docker_entrypoint.sh /usr/local/bin/healthcheck.sh
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/docker_entrypoint.sh"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ten31
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.
+23
View File
@@ -0,0 +1,23 @@
PKG_ID := ten-database
PKG_VERSION := 0.1.0.1
REPO_ROOT := $(abspath ../..)
WRAPPER_DIR := $(CURDIR)
IMAGE_NAME := start9/$(PKG_ID)/main:$(PKG_VERSION)
.PHONY: image-arm package verify clean
image-arm:
docker buildx build --platform=linux/arm64 \
-f $(WRAPPER_DIR)/Dockerfile \
-t $(IMAGE_NAME) \
-o type=docker,dest=$(WRAPPER_DIR)/image.tar \
$(REPO_ROOT)
package: image-arm
start-sdk pack
verify:
start-sdk verify s9pk $(PKG_ID).s9pk
clean:
rm -f $(WRAPPER_DIR)/image.tar $(WRAPPER_DIR)/$(PKG_ID).s9pk
+23
View File
@@ -0,0 +1,23 @@
# Start9 Wrapper (0.3.5)
This directory contains the StartOS 0.3.5 package wrapper for Ten31 Database.
## Build prerequisites
- Docker with buildx
- `start-sdk` installed on build machine
## Build package
```bash
cd /Users/macpro/Projects/CRM
make -C start9/0.3.5 package
```
## Verify package
```bash
cd /Users/macpro/Projects/CRM
make -C start9/0.3.5 verify
```
## Outputs
- `start9/0.3.5/image.tar`
- `start9/0.3.5/ten-database.s9pk`
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
DATA_DIR="${CRM_DATA_DIR:-/data}"
SECRET_FILE="$DATA_DIR/.crm-secret"
mkdir -p "$DATA_DIR" "$DATA_DIR/backups"
if [ -z "${CRM_SECRET_KEY:-}" ]; then
if [ -f "$SECRET_FILE" ]; then
CRM_SECRET_KEY="$(cat "$SECRET_FILE")"
else
CRM_SECRET_KEY="$(head -c 48 /dev/urandom | base64 | tr -d '\n' | tr '/+' 'ab')"
printf '%s' "$CRM_SECRET_KEY" > "$SECRET_FILE"
chmod 600 "$SECRET_FILE"
fi
export CRM_SECRET_KEY
fi
exec python3 /app/backend/server.py
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
PORT="${CRM_PORT:-8080}"
curl -fsS "http://127.0.0.1:${PORT}/api/health" >/dev/null
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
# Ten31 Database (StartOS 0.3.5)
## What this package does
- Runs Ten31 Database as a private web app.
- Persists all data under the StartOS service volume (`/data`).
- Exposes web UI/API on internal port `8080`.
## First launch
1. Open the service UI from StartOS.
2. If this is a fresh install, create the first admin account from the login screen.
3. Go to Settings and run a manual backup once.
## Airtable migration
1. Open Settings -> Migration.
2. Choose "Import from Airtable CSV".
3. Confirm row/column mappings before final import.
## Data safety
- Database path in container: `/data/crm.db`.
- Backups path in container: `/data/backups/`.
- Before StartOS or package upgrades, run a backup and export from Settings.
## Upgrade note
This 0.3.5 wrapper keeps app/runtime files separate from data volume so migration to a future 0.4 wrapper can preserve the same data directory layout.
+95
View File
@@ -0,0 +1,95 @@
id: ten-database
title: Ten31 Database
version: 0.1.0.1
release-notes: >-
Initial StartOS 0.3.5 package wrapper for Ten31 Database.
license: MIT
wrapper-repo: https://github.com/ten31/ten31-database-startos
upstream-repo: https://github.com/ten31/ten31-database
support-site: https://github.com/ten31/ten31-database/issues
marketing-site: https://ten31.vc
build: ["make image-arm"]
min-os-version: 0.3.5
description:
short: Self-hosted investor and fundraising database for Ten31.
long: >-
Ten31 Database is an Airtable-style investor CRM with fundraising grid,
communications logging, views, backups, and CSV import. This package stores
all runtime data in the service volume for upgrade-safe persistence.
assets:
license: LICENSE
icon: icon.png
instructions: instructions.md
docker-images: image.tar
main:
type: docker
image: main
entrypoint: docker_entrypoint.sh
args: []
mounts:
main: /data
health-checks:
main:
name: API health
success-message: CRM API is responding.
type: docker
image: main
entrypoint: healthcheck.sh
args: []
inject: true
config: ~
dependencies: {}
volumes:
main:
type: data
interfaces:
main:
name: Web Interface
description: Browser UI and API for Ten31 Database.
tor-config:
port-mapping:
80: "8080"
lan-config:
8080:
ssl: false
internal: 8080
ui: true
protocols: [http]
backup:
create:
type: docker
image: main
system: false
entrypoint: sh
args:
- -c
- |
set -eu
rm -rf /backup/*
cp -a /data/. /backup/
mounts:
main: /data
backup: /backup
restore:
type: docker
image: main
system: false
entrypoint: sh
args:
- -c
- |
set -eu
cp -a /backup/. /data/
mounts:
main: /data
backup: /backup
actions: {}
+9
View File
@@ -0,0 +1,9 @@
# Start9 Wrapper (0.4 placeholder)
This directory is reserved for the StartOS 0.4 package wrapper.
Migration plan from 0.3.5:
1. Keep package id stable (`ten-database`) if StartOS migration path allows.
2. Keep mounted data directory contract unchanged (`/data/crm.db`, `/data/backups`).
3. Rebuild wrapper files against 0.4 packaging spec and verify with current start-sdk.
4. Test upgrade on a staging node using production backup restore before live cutover.
Executable
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# Private beta launcher (Tailscale-first)
# Usage: ./start_beta.sh [port]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PORT="${1:-${CRM_PORT:-8080}}"
ENV_FILE="${CRM_ENV_FILE:-$SCRIPT_DIR/.env.beta}"
if [ -f "$ENV_FILE" ]; then
# shellcheck disable=SC1090
set -a
. "$ENV_FILE"
set +a
else
echo "ERROR: Missing $ENV_FILE"
echo "Copy .env.beta.example to .env.beta and fill values first."
exit 1
fi
if [ -z "${CRM_SECRET_KEY:-}" ]; then
echo "ERROR: CRM_SECRET_KEY is required for beta/prod mode."
exit 1
fi
if [ "${#CRM_SECRET_KEY}" -lt 24 ]; then
echo "ERROR: CRM_SECRET_KEY is too short. Use at least 24+ chars."
exit 1
fi
export CRM_ENV="production"
export CRM_PORT="$PORT"
export CRM_HOST="0.0.0.0"
TAILSCALE_IP=""
if command -v tailscale >/dev/null 2>&1; then
TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n 1 || true)"
fi
if [ -z "${CRM_CORS_ORIGIN:-}" ]; then
if [ -n "$TAILSCALE_IP" ]; then
export CRM_CORS_ORIGIN="http://$TAILSCALE_IP:$PORT"
else
export CRM_CORS_ORIGIN="*"
fi
fi
echo ""
echo " Venture CRM - Private Beta"
echo " Mode: $CRM_ENV"
echo " Port: $PORT"
echo " CORS: ${CRM_CORS_ORIGIN}"
if [ -n "$TAILSCALE_IP" ]; then
echo " Tailscale URL: http://$TAILSCALE_IP:$PORT"
else
echo " Tailscale IP not detected. Run: tailscale ip -4"
fi
echo ""
cd "$SCRIPT_DIR"
python3 backend/server.py
+258
View File
@@ -0,0 +1,258 @@
# Venture Fund CRM — Project Context for Claude
You are continuing development on a self-hosted CRM system for a venture fund. Below is everything you need to know about what has been built, how it works, and what comes next.
---
## Business Context
- **Fund:** ~$200M AUM, currently fundraising for Fund II
- **Users:** Team of 5 people, accessing via browser on local network or remotely via Tailscale VPN
- **Current LPs:** 150 investors
- **Prospects:** 250+ being tracked
- **Migrating from:** Airtable (CSV exports available)
- **Core goals:**
1. Eliminate sensitive LP/prospect data from third-party servers (Airtable, CRMs)
2. Stop paying monthly subscription costs
3. Purpose-built tool for fundraising workflow: managing existing investors, tracking new prospects, raising capital
- **User:** Grant (grant@ten31.xyz)
---
## What Has Been Built (Sprint 1 — Complete)
A fully functional prototype with backend API, frontend UI, demo data, and utility scripts. Everything runs locally with zero external dependencies beyond two Python packages.
### Tech Stack (Actual — differs from original plan)
The original plan called for FastAPI + SQLAlchemy + separate React build, but the build environment lacked pip/npm access. The stack was adapted to:
- **Backend:** Python 3 stdlib HTTP server + `sqlite3` + `bcrypt` + `PyJWT` — single file, no framework
- **Database:** SQLite with WAL mode (concurrent reads, serialized writes — fine for 5 users)
- **Frontend:** Single self-contained HTML file loading React 18 + Babel from CDN (unpkg)
- **Deployment:** Run `python3 backend/server.py` — serves both API and frontend on port 8080
- **Remote access:** Tailscale mesh VPN (each device gets a private IP, peer-to-peer encrypted)
### Project Structure
```
venture-crm/
├── backend/
│ ├── server.py # Complete API server (1,873 lines)
│ └── requirements.txt # bcrypt, PyJWT (for reference)
├── frontend/
│ └── index.html # Complete React SPA (2,982 lines)
├── data/
│ └── crm.db # SQLite database (created on first run)
├── scripts/
│ ├── create_user.py # CLI tool to add users
│ ├── reset_password.py # CLI tool to reset passwords
│ └── backup.sh # Database backup with 30-day retention
└── start.sh # Launch script
```
### Database Schema
All tables use TEXT primary keys (8-char UUIDs). The database is at `data/crm.db`.
**Tables:**
- `users` — id, username, email, password_hash, full_name, role (admin/manager/member), is_active
- `contacts` — id, first_name, last_name, email, phone, mobile, title, organization_id (FK), contact_type (investor/prospect/advisor/other), status, source, tags (JSON), notes, linkedin_url, preferred_contact, created_by (FK)
- `organizations` — id, name, type, industry, website, phone, email, address, city, state, country, description, tags (JSON), created_by (FK)
- `opportunities` — id, name, contact_id (FK), organization_id (FK), stage (lead/outreach/meeting/due_diligence/committed/funded), commitment_amount, expected_amount, probability, expected_close_date, fund_name, description, next_step, owner_id (FK), priority (low/medium/high), lost_reason
- `communications` — id, contact_id (FK), opportunity_id (FK), type (email/call/meeting/note/text), subject, body, communication_date, duration_minutes, outcome, next_action, next_action_date, attendees (JSON), created_by (FK)
- `lp_profiles` — id, contact_id (FK, unique), commitment_amount, funded_amount, commitment_date, fund_name, investor_type, accredited, legal_docs_signed, signed_date, wire_received, wire_date, k1_sent, preferred_communication, notes
- `custom_fields` — id, name, entity_type, field_type, options (JSON), required, display_order
- `custom_field_values` — id, custom_field_id (FK), entity_id, entity_type, value
- `audit_log` — id, user_id (FK), entity_type, entity_id, action, changes (JSON), created_at
- `tags` — id, name (unique), color
**Key indexes:** contacts(contact_type, status, organization_id), opportunities(stage, owner_id, contact_id), communications(contact_id, communication_date), audit_log(entity_type, entity_id), lp_profiles(contact_id)
### API Endpoints
All endpoints except auth require `Authorization: Bearer <jwt_token>` header. Server runs at `http://0.0.0.0:8080`.
**Auth:**
- `POST /api/auth/login` — body: {username, password} → {token, user}
- `POST /api/auth/register` — body: {username, password, email, full_name} → {token, user}
**Contacts:**
- `GET /api/contacts?type=&status=&search=&sort=&order=&limit=&offset=&organization_id=&tag=` → {data[], total, limit, offset}
- `GET /api/contacts/:id` → {data: {contact + communications[], opportunities[], lp_profile}}
- `POST /api/contacts` — full CRUD
- `PUT /api/contacts/:id`
- `DELETE /api/contacts/:id`
**Organizations:**
- `GET /api/organizations?search=&type=&limit=&offset=` → {data[], total}
- `GET /api/organizations/:id` → {data: {org + contacts[], opportunities[]}}
- `POST /api/organizations` — full CRUD
- `PUT /api/organizations/:id`
- `DELETE /api/organizations/:id`
**Opportunities (Pipeline):**
- `GET /api/opportunities?stage=&owner_id=&search=&priority=&fund_name=&limit=&offset=` → {data[], total}
- `GET /api/opportunities/:id` → {data: {opp + communications[], stage_history[]}}
- `POST /api/opportunities`
- `PUT /api/opportunities/:id`
- `PATCH /api/opportunities/:id/stage` — body: {stage} (logs stage change in audit)
- `DELETE /api/opportunities/:id`
**Communications:**
- `GET /api/communications?contact_id=&type=&search=&limit=&offset=` → {data[], total}
- `GET /api/contacts/:id/communications` → same as above, scoped to contact
- `POST /api/communications`
- `PUT /api/communications/:id`
- `DELETE /api/communications/:id`
**LP Profiles:**
- `GET /api/lp-profiles?fund_name=&search=` → {data[], total}
- `GET /api/lp-profiles/:id` → {data}
- `POST /api/lp-profiles` — also sets contact type to 'investor'
- `PUT /api/lp-profiles/:id`
**Reports:**
- `GET /api/reports/dashboard` → {metrics, pipeline_stages[], recent_communications[], upcoming_actions[], recent_stage_changes[]}
- `GET /api/reports/pipeline` → {by_stage[], by_owner[], by_priority[]}
- `GET /api/reports/lp-breakdown` → {lps[], summary, by_type[]}
- `GET /api/reports/activity?days=30` → {by_user[], by_day[]}
**Import/Export:**
- `POST /api/import/csv` — body: {data: [...objects], entity_type, mapping: {csv_col: crm_field}, dry_run: bool}
- `GET /api/export/contacts` → {data[]}
**Other:**
- `GET /api/tags` / `POST /api/tags`
- `GET /api/users`
- `GET /api/audit-log?entity_type=&entity_id=`
- `GET /api/health`
### Frontend Pages
The frontend is a single HTML file with inline CSS (dark theme) and React via CDN. Pages:
1. **Login** — username/password form, registration option
2. **Dashboard** — KPI cards (Total LPs, Committed $, Pipeline Value, Active Opportunities, Prospects, Monthly Comms), pipeline stage visualization, recent communications, upcoming actions, recent stage changes
3. **Contacts** — tabbed (All/Investors/Prospects), searchable sortable table, slide-over detail panel with communications timeline and opportunities, add/edit modal
4. **Pipeline** — Kanban-style board (Lead → Outreach → Meeting → DD → Committed → Funded), stage summary bar with $ per stage, opportunity cards with stage selector, add/edit modal
5. **Communications** — chronological list, filter by type/contact, log new communication form
6. **LP Tracker** — summary cards (Total Committed, Funded, Avg Check, LP Count), table with status indicators (checkmarks) for docs/wire/K1
7. **Import** — CSV paste/upload, preview table, field mapping interface, dry-run validation, execute import
8. **Settings** — user profile, tag management
### Demo Data (Seeded Automatically)
On first run, the server seeds:
- 2 users: `admin`/`admin123` (admin role), `grant`/`password` (admin role)
- 8 organizations (Sovereign Wealth Holdings, Pacific Capital Partners, Northeast Pension Fund, Redwood Endowment, Atlas Family Office, Summit Insurance Group, Cascade Wealth Management, Blue Harbor Foundation)
- 12 contacts (6 investors, 6 prospects)
- 6 LP profiles totaling $83M committed (all Fund I, all fully funded)
- 6 pipeline opportunities totaling $40M expected (Fund II prospects at various stages)
- 8 communication records (emails, calls, meetings)
- 6 tags (High Priority, Fund I LP, Fund II Prospect, Family Office, Institutional, Re-up Target)
### How to Run
```bash
pip3 install bcrypt PyJWT
cd venture-crm
python3 backend/server.py
# Open http://localhost:8080
# Login: grant / password
```
### What Has Been Tested
All API endpoints have been verified via curl:
- Auth (login, register)
- Contact CRUD + search
- Organization CRUD
- Opportunity CRUD + stage changes
- Communication CRUD
- LP profile CRUD
- Dashboard, pipeline, LP breakdown reports
- CSV import with dry-run and field mapping
- Frontend serves correctly from the backend
---
## What Has NOT Been Built Yet (Remaining Sprints)
### Sprint 2 items still needed:
- Custom fields UI (backend schema exists but not wired to frontend forms)
- Drag-and-drop on pipeline board (currently uses dropdown stage selector)
### Sprint 3: Airtable Migration + Custom Fields
- Custom field definition admin UI
- Display custom fields on contact/opportunity forms
- Actual Airtable data migration (import wizard exists but hasn't been used with real data)
### Sprint 4: Reporting + Polish
- Pipeline analytics (deal velocity, conversion rates between stages)
- User activity report page
- CSV export buttons on all reports
- Bulk actions on contact list (tag multiple, assign, bulk export)
- Automated daily backup via cron
- Team setup documentation
### Future Enhancements (discussed but not planned):
- Email integration (auto-log emails via IMAP)
- Calendar sync
- Task assignments linked to opportunities
- Bulk email with templates
- Two-factor authentication
- Advanced saved search filters
- Audit trail UI page
---
## Architecture Decisions & Constraints
1. **Single-file backend:** The Python server is one file (`server.py`) using stdlib `http.server`. No framework. This keeps deployment dead simple but means no middleware pattern, no auto-docs, no async. If the codebase grows significantly, consider migrating to FastAPI.
2. **Single-file frontend:** The React app is one HTML file loading from CDN. No build step. This means no TypeScript, no tree-shaking, no code splitting. Babel compiles JSX in the browser. If the UI grows significantly, consider splitting into a proper Vite/React project.
3. **SQLite WAL mode:** Handles 5 concurrent readers + 1 writer. Fine for this team size. If the team grows past 10-15, migrate to PostgreSQL.
4. **No localStorage:** JWT token stored in React state only (memory). Page refresh = re-login. This is intentional for security.
5. **8-char UUIDs:** Generated via `uuid.uuid4()[:8]`. Collision probability is negligible at this data scale.
6. **Tailscale for remote access:** Server binds to 0.0.0.0. Tailscale gives each device a 100.x.x.x IP. No port forwarding, no public exposure.
---
## Key Files to Read
When making changes, these are the files:
- **`backend/server.py`** (1,873 lines) — ALL backend logic: database schema, auth, every API endpoint, seed data, server startup. Search for handler method names like `handle_list_contacts`, `handle_create_opportunity`, etc.
- **`frontend/index.html`** (2,982 lines) — ALL frontend logic: CSS styles, React components, API client, every page. Search for component names like `Dashboard`, `ContactsPage`, `PipelinePage`, etc.
- **`scripts/create_user.py`** — CLI to add team members
- **`scripts/backup.sh`** — Database backup with rotation
- **`start.sh`** — Launch script
---
## Common Modification Patterns
**Adding a new field to contacts:**
1. In `server.py`: add column to CREATE TABLE, add to INSERT/UPDATE in handler methods
2. In `index.html`: add field to the contact form component and detail view
3. Delete `data/crm.db` to recreate schema (or use sqlite3 ALTER TABLE)
**Adding a new pipeline stage:**
1. In `server.py`: add to `PIPELINE_STAGES` list
2. In `index.html`: add to the stages array in the Pipeline component
**Changing the color scheme:**
1. In `index.html`: modify the CSS variables in the `<style>` tag (search for hex colors like `#0f172a`, `#1e293b`, `#6366f1`)
**Adding a new API endpoint:**
1. In `server.py`: add route matching in `do_GET`/`do_POST`/etc., then add handler method
**Adding a new page:**
1. In `index.html`: create a new component, add it to the navigation sidebar and the page router