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