Add in-app camera business-card intake (#7) (v0.1.0:100)

A mobile, in-app twin of the Matrix business-card flow (M3): photograph a
card in the app and it becomes a reviewed fundraising-grid add/note, with a
human approving every write.

Server — POST /api/intake/card (authenticated member+, read-only): lazily
imports the bot's nio-free parse + spark core, vision-transcribes the photo
(local VL via Spark Control — nothing to Claude), runs the same email/phone/
LinkedIn integrity rule + fuzzy matcher, and returns a proposal plus exact
match / fuzzy candidates. No write happens here.

Frontend — a camera button in the mobile top bar (left of the quick-log
pencil) → take or pick a photo → <canvas> downscale to JPEG (also normalizes
iPhone HEIC) → the endpoint → an editable review sheet (proposal fields +
existing-investor picker). Save reuses /api/fundraising/log-communication
tagged source="app_card".

No schema change, no migration, no new dependency, no Matrix-bot change. The
camera/canvas/OCR path is on-device-only (jsdom has no canvas); covered by
test_intake_card.py (stubbed vision+parse) + the render/mount smokes.
This commit is contained in:
Keysat
2026-06-20 14:15:03 -05:00
parent 2a4c2c25a0
commit 463f624548
6 changed files with 615 additions and 4 deletions
+3 -2
View File
@@ -64,8 +64,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
// * 0.1.0:96 (login page mobile/PWA conformance — the one surface the v95 mobile redesign skipped. CSS-only: 100vh→100dvh [dynamic viewport, fixes the centered card tucking under the iOS standalone status bar], a <768px media query adding 16px screen gutters + env[safe-area-inset] top/bottom clearance + touch-sized fields [inputs 46px/15px, button 46px], full-bleed card on small phones, and the §4 card depth shadow on the login card to match .section. No markup/JS/schema change; desktop login unchanged)
// * 0.1.0:97 (mobile top-bar polish + native zoom behaviour. Viewport meta gains maximum-scale=1 + user-scalable=no: kills pinch-zoom AND the iOS auto-zoom-on-focus that jerked the page in on every <16px input tap [app-wide, not just login]; OS accessibility zoom still works. Top-bar account initial now flex-centered + dc-aligned [IBM Plex Mono, accent-light, 13px — was defaulting to inline/baseline, off-center]. Quick-log pencil bumped --text-muted→--text-secondary for real affordance [the dc t3 grey thin-outline read as empty next to the color sun emoji on-device]. CSS-only; no JS/schema change)
// * 0.1.0:98 (business-card intake [Matrix bot] captures a contact's phone, mobile/cell, city + LinkedIn from a scanned card onto the contact record — cell to Mobile, office to Phone, fax skipped. Server half: _upsert_contact_from_fundraising now accepts phone+mobile on the contact dict [city+linkedin already worked]. The bot's transcription/extraction/card changes ship on the Spark [git pull + rebuild]. No schema change [contacts columns already exist]; no user-facing CRM change)
// * Current: 0.1.0:99 (Grant device-test round 2, CRM half: intake fuzzy match scores DISTINCTIVE tokens only [no more "Investment Group"/"Capital"/"Family Office" false look-alikes]; mobile grid "Last contact"/staleness sort is reversible; mobile Edit-investor prefills a contact's email [GET /api/fundraising/state heals a blank grid pill from the linked classic contact, fill-only]; mobile quick-log pencil icon renders [CSS sizing on the sole flex-child svg]. The Matrix intake thread-redaction change ships on the Spark, not here. No schema change; no migration)
export const PACKAGE_VERSION = '0.1.0:99'
// * 0.1.0:99 (Grant device-test round 2, CRM half: intake fuzzy match scores DISTINCTIVE tokens only [no more "Investment Group"/"Capital"/"Family Office" false look-alikes]; mobile grid "Last contact"/staleness sort is reversible; mobile Edit-investor prefills a contact's email [GET /api/fundraising/state heals a blank grid pill from the linked classic contact, fill-only]; mobile quick-log pencil icon renders [CSS sizing on the sole flex-child svg]. The Matrix intake thread-redaction change ships on the Spark, not here. No schema change; no migration)
// * Current: 0.1.0:100 (In-app business-card intake [#7]: a mobile camera button [left of the quick-log pencil] takes/picks a card photo, downscales it client-side via <canvas> to JPEG, and POSTs to the new POST /api/intake/card — vision-transcribe + parse + fuzzy-match on the box [local VL via Spark Control, nothing to Claude], reusing the Matrix card flow's nio-free parse/spark core. An editable review sheet [proposal fields + existing-investor picker] writes via log-communication tagged source="app_card"; a human approves every write. No schema change; no migration; no new dependency)
export const PACKAGE_VERSION = '0.1.0:100'
export const DATA_MOUNT_PATH = '/data'
export const WEB_PORT = 8080
+3 -2
View File
@@ -60,8 +60,9 @@ import { v_0_1_0_96 } from './v0.1.0.96'
import { v_0_1_0_97 } from './v0.1.0.97'
import { v_0_1_0_98 } from './v0.1.0.98'
import { v_0_1_0_99 } from './v0.1.0.99'
import { v_0_1_0_100 } from './v0.1.0.100'
export const versionGraph = VersionGraph.of({
current: v_0_1_0_99,
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98],
current: v_0_1_0_100,
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98, v_0_1_0_99],
})
+22
View File
@@ -0,0 +1,22 @@
import { VersionInfo } from '@start9labs/start-sdk'
// In-app business-card intake (#7) — a mobile, in-app twin of the Matrix card flow (M3):
// - Server: POST /api/intake/card (authenticated member+, read-only) lazily imports the bot's
// nio-free parse + spark core, vision-transcribes the photo (local VL via Spark Control —
// nothing to Claude), runs the same email/phone/LinkedIn integrity rule + fuzzy matcher, and
// returns a proposal + match/candidates. Nothing is written here.
// - Frontend: a camera button in the mobile top bar (left of the quick-log pencil) → take/pick a
// photo → <canvas> downscale to JPEG (also normalizes iPhone HEIC) → the endpoint → an editable
// review sheet. Save reuses log-communication tagged source="app_card"; a human approves.
// No schema change — no migration. No new dependency.
export const v_0_1_0_100 = VersionInfo.of({
version: '0.1.0:100',
releaseNotes: {
en_US: [
'In-app business-card scanning: tap the camera in the mobile top bar to photograph a card,',
'review the auto-filled name / email / title / phone / city, attach it to an existing investor',
'or add a new one, and save — the same on-box card reader the Matrix bot uses, now in the app.',
].join(' '),
},
migrations: { up: async () => {}, down: async () => {} },
})