Commit Graph

137 Commits

Author SHA1 Message Date
Keysat e3f5ef8dc8 docs: re-anchor mobile design on .dc.html defaults; scope Phase 8; refresh Current state
This session audited the mobile surfaces vs the Claude Design export
(functional-parity + visual-conformance) and corrected the design source of
truth: the *.dc.html prototypes at their default data-props
(compact/dark/plex/earmark) are canonical, not the screenshots (option-history).
Adds the 9-phase Phase 8 conformance plan to ROADMAP, a durable gotcha to the
Design convention, and prunes Current state to the live state.
2026-06-19 19:48:32 -05:00
Keysat 490cab92a3 Mobile Phase 7: theme-conformance pass — light-flip + retire legacy badges
Route remaining hardcoded UI colors through themed :root vars so they flip
under [data-theme="light"], finishing the P6 light-theme migration.

- Mobile: bottom-tab-bar -> --nav-bg; toast bottom-center above the tab bar,
  rising in via a new slideInUp keyframe.
- Money: --money for the kanban/pipeline-stage/stat amount literals.
- Badges: remap the legacy Material .badge-* family onto the brand
  StageChip/--chip-* + --badge-priority/-danger slots, retiring the second
  palette (DESIGN sections 2/7).
- Desktop-light: logout/danger buttons, table hover/stripe/footer, sidebar
  & header gradients, scrollbars, toast states, context-menu danger, thesis
  banner/accents, grid selection & drag indicators, skeleton loaders.

28 new themed vars (theme-stable ones dark-only); dark appearance preserved.
Frontend-only. Verified via CSSOM render-smoke, var resolution, brace check;
reviewer approve-with-nits (toast-rise fixed). Unverified on a real device.

Also refresh AGENTS.md Current state and add a ROADMAP legacy-usage-sweep note.
2026-06-19 18:28:49 -05:00
Keysat d16a567a3c docs: mark P3b committed in Current state (handoff) 2026-06-19 17:17:33 -05:00
Keysat 3f93daf28e Mobile P3b: investor name + contact-pill editing (update-row)
Adds the editable half of BRIEF §3a's mobile grid set: rename an investor
and add/edit/remove its contact pills from the mobile detail sheet.

New POST /api/fundraising/update-row is the one-row read-fresh-modify-write
twin of log-communication: it mutates only the target row's name/contacts in
the canonical grid blob server-side, then bumps the version + re-syncs the
relational tables. It never accepts a whole-grid payload, so a stale mobile
client can't clobber concurrent edits to other rows (the reason mobile avoids
the whole-grid PUT). _sanitize_fundraising_contacts whitelists the known pill
fields as the trust boundary; removing a pill is soft on the classic contacts
directory (only the grid pill + fundraising_contacts row drop).

Frontend: MobileFundraisingGrid gains an Edit bottom-sheet (name input + pill
editor with client-side dedup); money stays desktop-only. New CSS is
theme-var-only so it flips in light mode.

Verified: test_fundraising_update_row.py (24 assertions, real HTTP), full
suite 37/37, render-smoke + a 375px jsdom interaction harness green.
2026-06-19 17:07:29 -05:00
Keysat 099d87dad2 docs: refresh Current state + theme-var convention after P6 (handoff) 2026-06-19 16:46:46 -05:00
Keysat e6a89450da Mobile Phase 6: app-wide light theme + [data-theme] toggle
Ship the light palette behind a :root[data-theme="light"] switch; dark
stays the default and brand identity. A pre-paint boot script applies
localStorage.venture_crm_theme (no flash, no prefers-color-scheme), and an
app-wide toggle lives in the desktop sidebar footer + the mobile top bar,
both driven by one theme state in App.

Method keeps dark mode byte-identical: :root grew to 44 themed color slots
whose dark values equal the original literals, then 319 hex literals were
migrated to var() across the JSX inline region and the <style> block. The
StageChip is now className-based (.stage-chip--{stage}); PIPELINE_STAGE_CHIP
is removed. Every light tint (stage/recency/note/priority/reminder/money)
uses the designer's exact values from the full Claude Design export
(store.js + the four *App.dc.html DCLogic palettes), now committed as
provenance under design/_imports/2026-06-19_zip-file/ (zip + screenshots
gitignored).

Mobile surfaces + chrome are fully var-based, so mobile light is complete.
Desktop light has known rough edges (bespoke <style> shades, the legacy
off-palette .badge-* family, dark-tuned shadows) folded into a new Phase 7
design-conformance pass.

Verified: render-smoke green; a jsdom interaction harness on the authed
shell exercised the toggle (boot-dark -> light+persist+relabel -> dark);
dark-identity, theme-parity, and no-undefined-var checks all green. Not yet
checked on a real phone/browser.
2026-06-19 16:38:30 -05:00
Keysat 7f711d1fae docs: refresh Current state after P4/P5 commit (handoff) 2026-06-19 15:47:01 -05:00
Keysat ee9db6425a Mobile Phases 4–5: Pipeline (swipe-between-stages) + Reminders
Completes the four mobile-first surfaces. Both phases follow the established
useIsMobile() wrapper → Mobile*/Desktop* pattern; desktop is untouched (only
renamed Desktop*). No backend change.

P4 Pipeline (MobilePipeline): CSS scroll-snap swipe between the four stages +
count-forward segmented control + page dots; per-card ‹/› stage move and
tap → full-screen opp detail with a stage-picker sheet. Opp-centric — shares
PATCH /api/opportunities/{id}/stage with the desktop board and the Grid's
stage edit; view + advance-stage only (removal stays on the Grid/desktop board).

P5 Reminders (MobileReminders): urgency-grouped list over /api/reminders
(Overdue/Due soon/Later/Done/Cancelled) with an Active/Done/All filter; each
row is a pointer-drag swipe (left = mark done, right = snooze +7d, keeping
status open and pushing due_date, per the desktop rationale); tap → create/edit
BottomSheet (investor is create-only, matching the backend PATCH field set).
formatDueShort/reminderDueDelta fix the desktop formatter mis-rendering future
due dates.

Verified: render-smoke + throwaway jsdom 375px interaction harnesses (12/12
each); reviewer passes applied — notably P5 pointercancel no longer fires a
spurious mark-done. Deploy-pending (no s9pk built); not yet tested on a phone.
2026-06-19 15:44:49 -05:00
Keysat 95beb7bb19 docs: refresh Current state after P2/P3a (prune per-phase detail into ROADMAP) 2026-06-19 14:54:03 -05:00
Keysat e34a6fc672 Mobile Phase 3a: read + write-supported Fundraising Grid surface
Adds the mobile-first Fundraising Grid (<768px): a lean MobileFundraisingGrid
that reads /api/fundraising/state once and renders an investor card list over
the active view (name, committed $, pipeline-stage chip, staleness-colored
recency, Existing-Investor accent, Priority corner; graveyard muted) with a
bottom-sheet view picker and search. Tap a card -> full-screen detail with
read-only commitments/contacts/notes plus edit sheets: log a note, pipeline
stage, set a reminder, and a "+ New" investor create flow with client-side
dedup typeahead.

All writes go through the targeted one-row endpoints (log-communication,
pipeline link, opportunities stage PATCH, reminders) — NEVER the whole-grid
PUT, which would race the multi-user grid (BRIEF §3a). FundraisingGridPage is
now a useIsMobile() wrapper over the renamed-but-untouched desktop grid and
the new mobile one (rules-of-hooks-safe; desktop unchanged).

Backend: inject a read-only opportunity_id into grid rows
(opportunity_id_by_source_row; added to both strip points) so the mobile detail
can PATCH a linked opp's stage directly. Earliest-opp-wins ordering keeps it
consistent with pipeline_stage and the link's canonical pick.

Editing an existing investor's name + contact pills stays read-only here
(deferred to P3b — needs a narrow per-row PATCH + pill editor).

Tests: test_grid_pipeline_link extended (opportunity_id inject/strip/round-trip);
36/36 backend green, render-smoke green.
2026-06-19 14:49:49 -05:00
Keysat 984b950f80 Mobile Phase 2: read-only Contacts surface + shared BottomSheet/useIsMobile
Builds the mobile-first Contacts surface (<768px): a read-only A-Z directory
(sticky last-name letter headers) + segmented All/Investors/Prospects tabs +
pinned search -> full-screen detail (info with tap-to-copy email, opportunities,
comm history) -> a sort bottom-sheet. Contacts stays read-only on mobile per
design/BRIEF.md §3b (create/edit live on the Grid).

Lands the shared mobile primitives, deferred from Phase 1 and designed against
this first consumer (no dead code): <BottomSheet> (built on the Phase-1
.bottom-sheet CSS; scrim/Escape/pointer drag-to-dismiss) and useIsMobile()
(768px matchMedia). ContactsPage becomes a rules-of-hooks-safe wrapper that
mounts MobileContactsPage or the renamed-but-untouched DesktopContactsPage, so
desktop is unchanged. New CSS is JS-gated to the mobile component; grew the
:root/mobile var set per DESIGN §9 instead of hand-picking hexes.

Verified: render-smoke green + a throwaway jsdom interaction harness mounting
the real app at 375px (list/grouping/sort-sheet/detail/back, 14/14). Deploy-
pending (folds into the next s9pk with P0/P1/view-reorder).
2026-06-19 13:57:05 -05:00
Keysat 4ed16ca828 Refresh Current state + durable notes: Phase 0+1 built (4-stage enum + mobile foundation) 2026-06-19 13:38:47 -05:00
Keysat 634fc4260f Mobile foundation (Phase 1) + harden opportunity stage validation
Phase 1 mobile foundation (additive, no desktop change): :root mobile vars, a
4-tab bottom nav bar + mobile account/logout popover wired into App, a
bottom-sheet CSS primitive, and .mobile-only/.desktop-only utilities -- all
display:none >=768px. The <BottomSheet> React component + useIsMobile() + the
per-surface 15px type bump are deferred to Phase 2 (first use); light theme to
Phase 6.

Review hardening (fresh-eyes pass on the Phase 0+1 diff): validate stage in
handle_create_opportunity + handle_update_opportunity against PIPELINE_STAGES --
the narrower 4-stage enum makes a stale-client write of a legacy value invisible
to the report ORDER BY CASEs and unsettable from the UI. Use the canonical
pipelineStageLabel in the opportunity detail select; document the intentional
graveyard omission in the existing_investor / staleness helpers.

Tests: stage-validation regression in test_grid_pipeline_link.py + empty
source_row_id guard in test_pipeline_stages_v2.py; 36/36 green, render-smoke green.
2026-06-19 13:15:53 -05:00
Keysat e46dd36517 Pipeline funnel v2: 4-stage enum + migration 0007 + derived grid signals
Collapse the inherited 6-stage opportunity funnel to the locked 4-stage
per-investor funnel (lead -> engaged -> diligence -> commitment), terminal at
commitment. Migration 0007 remaps existing stage values (outreach/meeting ->
engaged, due_diligence -> diligence, committed/funded -> commitment) and
archives the stray 'lost' value (the grid row is left intact). Inject read-only
existing_investor (total_invested>0), last_activity_at, and staleness
(''/'aging'>=30d/'stale'>=60d) into the grid GET, stripped on write. Frontend:
4-stage chip tints + Pipeline board / opp-form / mock on the new enum.

The visible desktop existing-investor star + staleness recency column + the
Stale saved view are deferred to mobile Phase 3 (data is injected + test-locked
now, so that phase stays pure-frontend). Longshot was already retired by prior
cleanup -- no-op.

Tests: test_pipeline_stages_v2.py (migration remap + derivation boundaries) +
updated grid-pipeline-link / soft-delete / nl_query; 36/36 green, render-smoke
green, fresh-DB migrate clean.
2026-06-19 12:54:12 -05:00
Keysat fe62df1a14 Refresh Current state: mobile design distilled into contract; implementation planning next 2026-06-19 11:40:07 -05:00
Keysat 7b560c97b6 Distill mobile-first design round-trip into the contract
Phase C/D of the /design round-trip (Claude Design "Venture-CRM mobile
redesign", 2026-06-19). Captures the cloud output and folds it into the
durable design/ contract; no frontend reskin in this pass.

- _imports/2026-06-19/: provenance — GridApp.dc.html (byte-exact canonical
  surface) + a manifest README (project URL/inventory, data model, derived-
  field formulas, per-surface interaction model). DesignSync can't bulk-
  download, so screenshots/other sources stay recoverable from the cloud URL.
- DESIGN.md: §8 Responsive rewritten to the landed mobile-first system
  (4-tab bottom bar, card/detail, bottom sheets, swipe/snap, safe areas);
  §4 mobile component states; §3 15px mobile type scale; §2 stage/staleness
  + light-theme palette pointers.
- tokens.tokens.json: new `mobile` group (type scale, radii, touch sizing,
  safe-area) + `motion.sheet`; `color.light` palette — light theme adopted
  as a planned, toggle-gated feature (dark stays default).
- ROADMAP.md: mobile-first implementation backlog (contract-vs-code gap),
  gated on the inline-style->CSS migration and the locked pipeline spec.
2026-06-19 11:25:25 -05:00
Keysat d388464fe4 Refresh Current state: mobile-first redesign thread + locked pipeline spec 2026-06-19 10:52:45 -05:00
Keysat 9777fe6e25 Align BRIEF.md grid card with locked investor model
Drop 'type badge'/INVESTOR-PROSPECT category and the create-flow 'type'
field (no investor type — Existing-Investor is auto-derived from
committed $). Card now specs the existing-investor star/accent, Priority
as the only corner badge, the 4-stage chip shown only when in pipeline,
and last-contact staleness (grey->amber->red, 'Nd stale').
2026-06-19 09:13:59 -05:00
Keysat 168336c318 Capture one-off feature batch + lock pipeline-stages/flags redesign spec
Triaged eight one-off ideas (2026-06-18) into ROADMAP; #6 (spark-control
dashboard card) routed to standards/INBOX. Sharpened the pipeline-stages
idea into a locked spec (2026-06-19): 4-stage per-investor funnel
(Lead/Engaged/Diligence/Commitment), auto-derived Existing-Investor flag,
Priority+Graveyard disposition (Longshot dropped), staleness as a derived
recency overlay + W1b Matrix nudge (never auto-demotion), one global stale
threshold, and the card-presentation decisions. AGENTS Current-state notes
the built-pending view reorder + the captured batch.
2026-06-19 08:57:15 -05:00
Keysat c7f959d7d5 Add drag-to-reorder for fundraising grid views
Sidebar view list is now drag-reorderable (HTML5 DnD mirroring the
column-reorder idiom: moveViewBefore + draggingViewId/dragOverViewId).
Order persists via the grid page's existing autosave (views is already
in its snapshot + deps), the same path rename/delete use; no backend
change. Render-smoke green.
2026-06-19 08:57:15 -05:00
Keysat 99404db48b Add mobile-first design contract and redesign brief
Scaffold design/ for the frontend's first design contract, extracted
as-built from index.html (document-as-is):
- DESIGN.md: 9-section brand brief (dark venture-CRM look, IBM Plex,
  single #3b82c4 accent) + tokens.tokens.json (DTCG, from :root + an
  inline-style census).
- BRIEF.md: the mobile-first redesign packet. Mobile = 4 surfaces
  (Grid, Pipeline, Reminders, Contacts) in a bottom tab bar; the rest
  desktop-only. Grid view-switching first-class; narrow on-the-go edit
  set (name, contacts, notes/comms/outreach log, stage, reminders) +
  create-investor, all via the canonical grid path (Contacts stays
  read-only). Includes a backend-reality callout: no field-level write
  (whole-grid versioned PUT vs the targeted log-communication path),
  stage is a separate two-call opportunities flow, pill removal has no
  undo, dedup typeahead is client-side.
- brand/ assets, inspiration/ provenance.

Wire the AGENTS.md Design line so any agent reads the contract before
UI work.
2026-06-18 21:50:34 -05:00
Keysat ab0d82ff00 Mark v0.1.0:94 deployed (NL-query matched-only fix live on the box) 2026-06-18 20:27:38 -05:00
Keysat 9d0d3068fb Bump package version to v0.1.0:94 (NL-query matched-only fix)
Ships the comms_by_user / email_counts_by_user matched-only fix to the box.
No schema change, no UI change — version migrations are no-ops.
2026-06-18 20:25:34 -05:00
Keysat 2d43bad6fc Restrict comms_by_user/email_counts_by_user to matched-investor email
Both NL-query intents counted/listed a user's ENTIRE captured sent corpus
(internal, vendor, personal mail) rather than only email to a matched investor
— they were missing the `EXISTS email_investor_links` gate that recent_emails
and the Communications panel's query_email_activity use. Their own docstrings
said "investor emails", so the behavior was wrong, not just loose.

Add the matched-only gate to both, mirroring query_email_activity. The runner
test now seeds an unmatched sent email and asserts it is excluded (without the
fix comms_by_user returns 3 not 2, this_week 2 not 1) — the prior fixture
linked every email, so the leak went uncaught.

Also documents the matched-only rule in the nl-query guide, and refreshes the
AGENTS.md Current state (v93 deployed; this fix pending a v94 s9pk since the
intents run on the box, not the bot).
2026-06-18 20:24:52 -05:00
Keysat f7b03ee109 Bump package version to v0.1.0:93 (reminders + NL-query)
Ships the next s9pk for the box, which jumps from v91 and so bundles two
in-repo-but-undeployed workstreams:
  - W1 reminders & follow-ups (v92): in-app migration 0006 (additive — a new
    `reminders` table + indexes; verified up/down against a copy of crm.db).
  - W2 natural-language query: read-only POST /api/query/nl + /api/query/catalog
    (require_bot_or_admin, audited), local-model translation, no schema change.

The Matrix Q&A client for W2 ships separately on the Spark and depends on this
endpoint being live on the box.
2026-06-18 19:56:53 -05:00
Keysat 68106d7a5a Add Matrix NL-query Q&A surface (W2 step 5)
Read-only natural-language query over the curated nl_query endpoint, answered
in-thread. Two entry points (room-per-purpose model): a dedicated Q&A room
(MATRIX_QUERY_ROOM) where every top-level message is a question, plus the
?/@bot trigger in the intake room as a cross-room convenience. Both routes hit
the same handle_query -> crm_client.nl_query -> POST /api/query/nl; translation
runs on the box's local model, nothing leaves the box, and there is no write
path so no approval gate applies.

Pure logic (trigger parsing, answer rendering) in query.py with offline tests;
async room wiring in bot.py (live-smoke only, per the bot's convention).

Bot-side only, ships on the Spark via git pull + restart. Depends on the
box-side /api/query/nl endpoint, which lands with the v93 s9pk (reminders + W2):
until v93 is installed the Q&A surface 404s, so the bot deploy is staged to
follow that install.
2026-06-18 19:46:54 -05:00
Keysat 6c29c22601 Add NL-query backend (W2): local translator + safe named-query runner
Read-only "ask the database in plain English" backend. Translation runs on
the local Qwen via Spark Control (question -> {intent, slots}); nothing leaves
the box, no Claude and no redaction boundary (the simplification chosen after
pressure-testing). The safe surface is a curated catalog of ~12 hand-written
parameterized queries; a slot validator is the trust boundary (no generic SQL,
no dynamic identifiers). POST /api/query/nl + GET /api/query/catalog, gated
require_bot_or_admin, read-only, audited. Soft-delete-correct per table.
Local Qwen translated 12/12 real example questions correctly against the live
Spark. Web "Ask" box and Matrix bot still to come (steps 4-5).
2026-06-18 18:35:41 -05:00
Keysat a166b49397 Handoff: record reminders W1 (v0.1.0:92, deploy pending); next = deploy + W2 NL-query 2026-06-18 14:48:17 -05:00
Keysat f181525926 Add reminders & follow-ups (W1) (v0.1.0:92)
First-class reminders tied to the fundraising grid — foundation of the agreed
reminders -> NL-search -> bot-mutations plan (keep LP data off third-party LLMs).

- reminders table (migration 0006; logical FK to fundraising_investors.id +
  denormalized name), CRUD at /api/reminders (soft-delete; open/done/snoozed/
  cancelled; assignee; source; source_row_id resolution)
- read-only derived reminder_status grid column (overdue/due_soon/open),
  filterable; orphan reconciler cancels reminders when an investor leaves the grid
- Reminders page, Dashboard "Reminders Due" card, daily-digest reminders section
- per-investor last_activity_at recency rollup (shared block for the W2 NL query)
- tests: test_reminders.py + digest reminders test (31/31 green, render-smoke green)
2026-06-18 14:45:46 -05:00
Keysat ee6a4e52d2 Handoff: email-proposal Matrix review live (v0.1.0:91); bot role + whole-thread redaction
Durable updates after the email-proposal review session:
- AGENTS.md: roles admin/member -> admin/member/bot; add a Conventions entry on
  the bot role and the reach(role)-vs-autonomy(approval gate) principle.
- matrix-intake guide: rewrite the bridge section to final behavior (redact_thread
  whole-thread redaction, the Element 'show deleted' client-setting dependency for
  full clearing, the redact_resolved.py backfill tool, deploy gotchas).
- Current state rewritten lean (14->8 bullets); test count 27->30.
2026-06-18 12:51:46 -05:00
Keysat b2690c4342 Redact whole review threads on decision (replies too)
The bot was granted a redact/mod power level in the review room, so it can now
clear a resolved thread entirely, not just the card: redact_thread redacts the
card root then scans recent history for its m.thread replies (the human's yes/no
+ any bot messages) and redacts those too, so decided threads drop out of the
threads view, not only the main timeline. Drops the in-thread confirmation on a
successful decision (the thread clearing is the ack; a confirmation would keep
the thread alive). redact_resolved.py extended to also clear replies of already-
resolved threads for the one-time backfill. Bot-only; no s9pk change.
2026-06-18 12:32:06 -05:00
Keysat 9044641b08 Add one-time tool to redact resolved review cards
Cards decided before the auto-redact behavior shipped are already 'closed' in
the CRM, so the bot's to_close sweep never redacts them. redact_resolved.py walks
the review room, keeps cards still pending (CRM 'open' list), and redacts the
rest. Dry-run by default; --apply to act. Run via docker compose on the Spark.
2026-06-18 12:09:48 -05:00
Keysat a10889b10b Refine email-proposal review UX (v0.1.0:91)
Three post-smoke refinements to the Matrix email-proposal review:

1. Dash separators (bot): every card/reply is framed with a dash rule top and
   bottom so threads stop bleeding together vertically on mobile.

2. Remove decided threads (bot): on a conclusive approve/dismiss from either
   surface, the bot redacts the card (client.room_redact) so the room clears
   down to only undecided items. Redacting the bot's own card needs no power;
   the web->Matrix path now redacts instead of posting a closure note.

3. Clearer note wording (server v91 + bot): the proposed grid note now names who
   emailed whom -- "{teammate} emailed {investor}" (outbound) / "{sender} emailed
   the team" (inbound) -- instead of an ambiguous "Sent"/"Received". Outbound
   detection also matches our corporate domain (public providers excluded), so a
   teammate's mail from a non-enrolled @ten31.xyz address no longer reads as
   "Received". Going-forward only; no schema change. The card drops its bare
   direction label since the note now carries the relationship.

Tests updated; 30/30 green, render-smoke green.
2026-06-18 11:59:38 -05:00
Keysat 48bd29aaa3 Record email-proposal Matrix review deployed & live (box v0.1.0:90) 2026-06-18 11:21:38 -05:00
Keysat 29987061cb matrix-intake: bot joins the email-review room on startup
room_send to a room the bot is only invited to (not joined) fails M_FORBIDDEN;
join explicitly on startup (idempotent if already a member). Bot-only change —
ships via the Spark git pull, no s9pk bump.
2026-06-18 10:33:19 -05:00
Keysat 27e9ea5b0b Add 'bot' to the admin edit-user role dropdown (v0.1.0:90)
v89 added the 'bot' role for the Matrix email-review bot's endpoints but kept it
out of the UI, leaving no click-path to assign it. Add 'bot' to the Settings ->
Admin edit-user role dropdown (the teammate-invite form stays member/admin only —
provisioning an agent account is an admin re-classification of a dedicated user,
not a teammate invite). The backend update validator already accepts 'bot'.
Frontend-only, no schema change.
2026-06-18 10:13:30 -05:00
Keysat 5faa5ae4d6 Email-proposal review over Matrix + a bot role (v0.1.0:89)
The email-capture "proposed grid notes" gain two review surfaces:

1. Inline source email — each proposed-note card on the Email Capture page
   gets a "View email" toggle that lazily fetches the existing
   GET /api/email/detail and shows from/to/cc/date/subject + scrollable body,
   so a reviewer can judge the note against the email it was drafted from.

2. CRM->Matrix review bridge — the CRM (box, stdlib, no matrix-nio) can't post
   to Matrix, so the intake bot (Spark) PULLS: GET /api/intake/email-proposals
   returns to_post/open/to_close work-lists; the bot posts a review card
   (metadata + snippet + draft note) to a dedicated review room
   (MATRIX_EMAIL_REVIEW_ROOM) and relays in-thread yes / no / NL-edit
   (POST .../{id}/decide, note revised via local Qwen). Decisions sync both
   ways: web decide -> bot announces + closes the thread; Matrix decide -> the
   web panel's ~25s poll clears the card. State lives CRM-side in the new
   email_proposal_matrix side row (email-integration migration 0003, additive
   + idempotent CREATE TABLE IF NOT EXISTS), so it survives a bot restart.

Adds a 'bot' role (authenticated, never admin; require_bot_or_admin) to gate
the email-proposal endpoints rather than handing the bot full admin — the
principled base for the coming agentic capabilities. Role controls reach;
the draft->approve gate still controls autonomy (a human approves every write).

Deploy split: endpoints + migration + role + frontend ship in the s9pk; the
bot poll loop + review-room handling ship on the Spark. The bot's CRM user
must be flipped member->bot and joined to the review room (one-time).

Tests: backend/test_email_proposal_matrix.py + matrix_intake/test_email_proposals.py
(30/30 suite green, render-smoke green, migration verified twice on a DB copy).
2026-06-18 09:51:41 -05:00
Keysat 41def0f014 Handoff: Adopt the Pipeline done — v88 verified live, full round-trip smoked
Box and repo on v0.1.0:88; the +Pipeline -> board -> advance-stage -> remove
round-trip is verified on the box. Pipeline adoption is closed out; ROADMAP
item marked done and the Next list advances to the spark-control intake card.
2026-06-18 08:34:32 -05:00
Keysat 114916b789 Retire the Pipeline page's "+ New Opportunity" button (v0.1.0:88)
Opportunities are now born only from a fundraising-grid investor row
("+ Pipeline"), which matches how the team works — they live in the grid,
not on the board. The old "+ New Opportunity" button created a deal by
picking a contact, a path that contradicts the grid-is-canonical model and
the contact-vs-investor framing.

Remove the button, its create-by-contact modal, the now-dead handler/state,
and the Pipeline page's unused /api/contacts fetch. Replace the button with a
muted "Add deals from the Fundraising Grid" hint. The board is now a view +
stage-management surface. Frontend-only; no backend or schema change.

Render-smoke green.
2026-06-18 08:25:14 -05:00
Keysat 4df104b119 Record Adopt-the-Pipeline deployed live (v0.1.0:87)
Box and repo now on v0.1.0:87: StartOS chain ...86->87 clean,
0005_grid_pipeline_link.sql applied on the box, server up on :8080.
Next is an in-room/board live-smoke of the Add-to-Pipeline round-trip.
2026-06-18 07:54:19 -05:00
Keysat 7f9a15ebf3 Adopt the Pipeline: grid-driven opportunities link (v0.1.0:87)
The fundraising grid (canonical) now drives the classic opportunities
Pipeline board, instead of the board being a disconnected second data-entry
surface. An "Add to Pipeline" row action creates a durably-linked opportunity
via the new opportunities.fundraising_investor_id (migration 0005, additive +
reversible), reusing the grid's already-synced contact — retiring the
POST /api/contacts side-door — and mapping the grid lead to the opp owner.

Ownership is split so the two stay reconciled: the grid owns whether the link
exists and the seed; the board owns stage/probability/owner. The link endpoint
is idempotent (one live opp per investor; a re-link never reseeds funnel
fields). "Is in pipeline?"/"what stage?" are derived from a live opp join and
injected as read-only grid columns on read, stripped on write, so they never
persist or dirty the autosave. Remove-from-pipeline soft-deletes the opp and
leaves the grid row fully intact; deleting an investor from the grid archives
its orphaned opp.

Also fixes the standing soft-delete leak in handle_pipeline_report and the
dashboard pipeline aggregates, which counted tombstoned opportunities.

Tests: backend/test_grid_pipeline_link.py (link/idempotent/round-trip/guards/
unlink-intact/re-link/orphan/aggregates); 28/28 suite green, render-smoke green.
2026-06-17 23:08:36 -05:00
Keysat 06482247df Handoff: record team-roster parse frame deployed & smoked; reprioritize Next 2026-06-17 22:17:44 -05:00
Keysat c1ea1769a4 Matrix intake: frame parse with team roster so a teammate isn't read as the prospect
Local-smoke found "jonathan is chatting with wyoming" extracted the teammate, not
the prospect. Feed the parser an optional team roster (INTAKE_TEAM_ROSTER) via a
build_system(roster) outreach frame: roster names/initials are the people doing
outreach and are never extracted; the other party is the investor/prospect. Same
framing on the revise leg. Unset roster = prior behavior.
2026-06-17 21:58:54 -05:00
Keysat b376b8ce33 Handoff: prune Current state to a snapshot; note shared .dockerignore gotcha 2026-06-17 20:30:39 -05:00
Keysat cae2dbc8b9 Record intake-bot containerization; log parse fix, card handoff, and repo-extraction follow-ons
Bot now runs as a docker-compose service on the Spark (verified live, listening). Docs (matrix-intake guide ops, ROADMAP, AGENTS Current state) updated. Also logs the live-smoke parse bug (teammate read as investor -> team-roster fix), the spark-control dashboard-card handoff, and the long-term dedicated-repo extraction.
2026-06-17 20:13:35 -05:00
Keysat b470ea2659 Containerize the Matrix intake bot as a managed service (restart: unless-stopped)
Turn the bot from a bare nohup process (silently dies on a Spark reboot) into a docker-compose service. Dockerfile bundles backend/matrix_intake + the stdlib backend/ingest Spark client it reuses; .env is mounted read-only at runtime, never baked. The existing repo-root .dockerignore (shared with the s9pk build) already keeps data/ and .env out of context. Also adds a handoff doc for wiring a spark-control dashboard card in a later session.
2026-06-17 20:10:16 -05:00
Keysat a7b03837b3 Record v0.1.0:86 deploy: Matrix intake fuzzy + conversational pass live on the box + Spark
Box installed to 0.1.0:86 (migration chain ...85->86 clean, candidates endpoint verified live); bot pulled + restarted on the Spark. Only the Matrix live-smoke remains.
2026-06-17 18:55:51 -05:00
Keysat 0b893295e1 Matrix intake: fuzzy investor matching + conversational in-thread edits (v0.1.0:86)
Close the two locked post-deploy enhancements for the Matrix intake bot.

Fuzzy matching (server-side, ships in the s9pk): new find_intake_candidates in
server.py returns ranked deterministic near-matches (difflib name similarity +
token-set Jaccard, legal-suffix-aware, + email Levenshtein <= 2); GET
/api/intake/match now returns {match, candidates}. The bot surfaces a numbered
shortlist so a near-duplicate (Charlie/Charles, Acme Capital vs Acme Capital LLC,
a one-char email typo) is confirmed by a human instead of silently creating a
second investor. Exact match still auto-attaches; fuzzy candidates are never
auto-attached. The optional LLM-judge re-rank is deferred.

Conversational edits (bot-side, ships on the Spark): any in-thread reply that
isn't yes/no/edit field=value is treated as a natural-language revision and
re-run through local Qwen (parse.revise). Email integrity is preserved -- a
changed address must literally appear in the instruction; the model's email
field is structurally unreachable. No-op revisions re-prompt.

Docs/current-state brought current; 27/27 backend tests green.
2026-06-17 18:50:58 -05:00
Keysat fa6c9da0e6 Drop redundant "[note]" tag from fundraising-grid note line (v0.1.0:85)
The grid note line was "YYYY-MM-DD [type] Contact: summary"; for the default
"note" type the tag is noise. Omit it for "note"; keep it for informative
types (call, meeting, …). Shared by the Matrix intake bot and grid-UI logging.
Built + installed to the box (installed-version 0.1.0:85, clean 84->85
migration). No schema change.
2026-06-17 17:30:40 -05:00
Keysat aefb2aa119 Matrix intake: main-timeline nudge, clearer messages, note text in grid
Four bot-side UX fixes surfaced by the live smoke:
- Post a brief pointer in the main timeline (a reply to the user's message)
  alongside the in-thread proposal card, so proposals aren't missed inside a
  thread. Pointer only — approvals still happen in the thread, where the note
  is visible (you can't make an informed yes/no without seeing it).
- A bare yes/no typed in the main timeline while a proposal is pending now
  gets a "reply in the thread" redirect instead of "couldn't tell what to record."
- Clearer commit confirmations: "Created a new grid entry for X" vs
  "Logged a note on X (existing grid entry)."
- Send a blank communication subject when a note is present so the grid's
  one-line note summary shows the note text, not the "(Matrix)" label
  (provenance stays in source="matrix_intake").
2026-06-17 17:14:08 -05:00