init local package repo
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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
@@ -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.
@@ -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
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
Binary file not shown.
@@ -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 |
@@ -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
File diff suppressed because it is too large
Load Diff
+3571
File diff suppressed because it is too large
Load Diff
+3559
File diff suppressed because it is too large
Load Diff
Executable
+48
@@ -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
|
||||
Executable
+71
@@ -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()
|
||||
Executable
+47
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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.
@@ -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.
|
||||
@@ -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: {}
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user