Files
ten31-database/ROADMAP.md
T
Keysat 2758ac81d3 Add daily-digest Phase A: per-package SMTP send + admin test endpoint (v0.1.0:75)
Groundwork for the daily activity digest: give the CRM an outbound mail path.
Today nothing leaves the box (Gmail capture + drafts only), so this adds a
dedicated, per-package SMTP account independent of any StartOS system-wide SMTP.

- configureDigestSmtp Start9 action: writes host/port/from/username/password/
  security to /data/secrets/smtp/* (password piped over stdin, never argv/env;
  per-field files, owner-only) — mirrors the setAnthropicApiKey pattern.
- docker_entrypoint.sh reads those at boot and exports SMTP_* (operator env wins).
- backend/smtp_send.py: stdlib smtplib wrapper reading SMTP_* (one code path for
  dev .env and the box); starttls/tls/none modes.
- POST /api/admin/digest/test-email (admin-only): proves the pipe. Recipients are
  restricted to the active-admin set — an arbitrary `to` is rejected, so the
  endpoint is not an open relay; send failures are logged, not echoed (an SMTP
  auth error can carry the credential).
- Tests: test_smtp_send.py (sender), test_smtp_endpoint.py (gating + relay
  restriction + no-leak). 18/18 backend green; s9pk typechecks.

Analysis/summarization for the digest body (Phase B) will run on Spark, never
Claude — the digest is deliberately un-anonymized. Decisions + Phase B plan in
ROADMAP.md.
2026-06-15 18:33:06 -05:00

8.5 KiB

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).
  1. Admin-invite auth model
  • Disable self-register for non-admin users.
  • Add admin-only invite/create-user endpoint.
  • Keep role model: admin, member.
  1. 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.
  1. 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
  1. Formula engine v2
  • Add functions: SUM, MIN, MAX, ROUND, ABS, CONCAT (done)
  • Type-aware formulas and better errors
  • Dependency graph and recalculation rules
  1. 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)

Backlog (post-Phase-1 agentic)

Daily activity digest (email to the team)

Requested 2026-06-15. Phase A built in v0.1.0:75 (outbound SMTP send capability + admin test-email endpoint; not yet deployed). Phase B (digest content + Spark summarization + daily scheduler) remains.

Decisions (locked 2026-06-15): recipients = all active admins; summarization = Spark-LLM narrative (never Claude — un-anonymized substance stays local); granularity = grouped by user (→ per investor).

Phase A — DONE (v0.1.0:75): configureDigestSmtp Start9 action writes a per-package SMTP account to /data/secrets/smtp/*; docker_entrypoint.sh exports SMTP_*; backend/smtp_send.py (stdlib smtplib) + admin POST /api/admin/digest/test-email (recipient-restricted to the admin set — not an open relay). Tests: test_smtp_send.py, test_smtp_endpoint.py.

Phase B — TODO: daily scheduler (co-locate with email_integration/scheduler.py); per-user→per-investor activity query (deleted_at IS NULL throughout); Spark-narrative summary of captured email substance; compose + send to all admins.

Have the CRM send a daily digest email summarizing each registered user's activity — primarily who emailed which investors and the substance of those emails — to the fund principal (and eventually other admins). Scales with the synced-user count: 2 users synced today, ~5 eventually.

  • Source data: the captured email-activity already flowing through the Gmail DWD propose→approve pipeline (backend/email_integration/), keyed per registered user → per investor/contact. Optionally fold in other CRM activity (audit feed, automation runs, new opportunities) later.
  • Send path is NEW capability. Today nothing leaves the box — the system only captures Gmail and creates drafts. This needs outbound SMTP. StartOS 0.4 has a system-wide SMTP account (since v0.4.0-beta.9): the user configures it once for the whole server and services read it via sdk.getSystemSmtp(effects).const(), which returns a T.SmtpValue (host, port, from, username, password, security). Wire the digest sender to that rather than hardcoding any account. Implementation path (researched 2026-06-15, our SDK pin ^0.4.0-beta.66): model a manageSMTP action on gitea-startos / vaultwarden-startos — a three-way selection (system / custom / disabled) built on sdk.inputSpecConstants.smtpInputSpec, persisted to storeJson, with main.ts injecting SMTP_HOST/PORT/USER/PASS/FROM/SECURITY env vars into the daemon exec block (same shape as the existing setAnthropicApiKey.ts action). The Python sender reads them via os.environ and opens smtplib.SMTP/SMTP_SSL. "Custom SMTP" is a dedicated per-package account, fully independent of the server's system SMTP — the custom branch never calls getSystemSmtp, so the digest can send through its own provider even on a box with no system account configured (confirmed in both reference packages). This is the likely fit here: a digest-only mailbox separate from anyone's Gmail. Note StartOS 0.4 dropped the old Config/Properties manifest spec — SMTP config is an action + storeJson, not a manifest config field. SDK note (verified 2026-06-15): our pin ^0.4.0-beta.66 resolves to exactly 0.4.0-beta.66 (caret on a prerelease stays within the 0.4.0 tuple), whose SMTP surface — getSystemSmtpT.SmtpValue {host, port, from, username, password, security}, inputSpecConstants.smtpInputSpec (providers gmail/ses/sendgrid/mailgun/protonmail/other; selection disabled/system/custom), smtpShape, smtpPrefill — is byte-identical to the 1.5.3 reference packages (verified from published tarballs; repo node_modules is absent). Build against beta.66 as-is — no SDK bump needed (moving to 1.x is a major-track change with broad blast radius across startos/, and nothing about SMTP justifies it).
  • Analysis runs on Spark, never Claude. The digest is deliberately un-anonymized (real LP names + email substance), so any summarization/analysis must go through Spark Control to local models — this is the one path that intentionally bypasses the scrub→Claude→re-hydrate boundary, because keeping the substance local is the whole point. Never route digest content to Claude.
  • Exempt from "agents draft, humans send." That rule governs outward LP/prospect contact. This is an internal ops digest to the team's own inboxes — a different category — so an automated daily send here does not violate the draft-only guardrail. State this explicitly at build time.
  • Scheduling: a daily cron, naturally co-located with the existing backend/email_integration/scheduler.py sync cadence.
  • Soft-delete: every aggregate/read in the digest must filter deleted_at IS NULL (see the standing soft-delete rule).

Open design questions (Phase B detail, still to settle): fixed daily send time; "nothing happened today" suppression; whether the Spark summary is per-investor-thread or a single per-user narrative.

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