merge upstream
875
.Agent Context/Bibels/Project Velocity Master Bibel.md
Normal file
@@ -0,0 +1,875 @@
|
|||||||
|
# Project Velocity Master Bibel
|
||||||
|
|
||||||
|
**Status:** Master source of truth
|
||||||
|
**Prepared:** 2026-04-12
|
||||||
|
**Scope:** Unified synthesis of Project Velocity markdown documentation across product, architecture, infrastructure, operations, iOS, web, AI workflows, and GTM context
|
||||||
|
**Exclusions:** Anything under `Sourik` was intentionally excluded from this synthesis
|
||||||
|
**Method:** This document normalizes and reconciles the current documentation corpus. It is not a raw concatenation. Where source docs disagree, this bibel favors the latest operational truth and the more concrete implementation artifact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chapter Index
|
||||||
|
|
||||||
|
1. Charter and Scope
|
||||||
|
2. Source Corpus
|
||||||
|
3. Product Thesis
|
||||||
|
4. Commercial Model
|
||||||
|
5. Customer and User Model
|
||||||
|
6. Suite Architecture
|
||||||
|
7. Core Product Surfaces
|
||||||
|
8. Oracle
|
||||||
|
9. Sentinel
|
||||||
|
10. Dream Weaver and Catalyst
|
||||||
|
11. Web Application
|
||||||
|
12. iOS Application
|
||||||
|
13. Backend and AI Runtime
|
||||||
|
14. Data Model and Core Entities
|
||||||
|
15. Infrastructure and Deployment Model
|
||||||
|
16. Stable Ingress Layer
|
||||||
|
17. Linux AWS Control Surface
|
||||||
|
18. Operating Flows
|
||||||
|
19. Security, Privacy, and Sovereignty
|
||||||
|
20. Current Live Truth
|
||||||
|
21. Build Priorities and Open Gaps
|
||||||
|
22. Runbooks and Team Usage
|
||||||
|
23. Source Lineage Map
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Charter and Scope
|
||||||
|
|
||||||
|
Project Velocity is not one app. It is a private, on-prem or client-controlled real-estate operating system composed of:
|
||||||
|
|
||||||
|
- a premium web interface
|
||||||
|
- a native iPad surface
|
||||||
|
- an AI CRM and analytics layer
|
||||||
|
- a biometric perception and scoring layer
|
||||||
|
- a visual generation layer
|
||||||
|
- a durable infrastructure and operator layer
|
||||||
|
|
||||||
|
Velocity is designed around an anti-SaaS principle:
|
||||||
|
|
||||||
|
- client data stays with the client
|
||||||
|
- deployments can run on-prem or in client-controlled cloud
|
||||||
|
- the product is sold as strategic capability, not seat-based software
|
||||||
|
|
||||||
|
This master bibel exists to collapse scattered markdown artifacts into one normalized document the team can use as the primary reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Source Corpus
|
||||||
|
|
||||||
|
This bibel synthesizes the key Project Velocity markdown sources, including:
|
||||||
|
|
||||||
|
- core bibles:
|
||||||
|
- `velocity_technical_bible.md`
|
||||||
|
- `velocity_ios_bible.md`
|
||||||
|
- `Project Velocity - The Oracle.md`
|
||||||
|
- `The Sentinel Bibel.md`
|
||||||
|
- `Desineuron Ops Control Plane Bibel.md`
|
||||||
|
- infrastructure:
|
||||||
|
- `TEAM_HANDOFF_2026-04-08.md`
|
||||||
|
- ingress and ops control plane READMEs
|
||||||
|
- Comfy and Dream Weaver:
|
||||||
|
- `DREAMWEAVER_TECHNICAL_SPEC.md`
|
||||||
|
- `A100_DEPLOYMENT_VALIDATION.md`
|
||||||
|
- `comfy_engine/scripts/README.md`
|
||||||
|
- operational truth docs:
|
||||||
|
- `nemoclaw_setup_truth.md`
|
||||||
|
- `velocity_status_report.md`
|
||||||
|
- `oracle_development_status.md`
|
||||||
|
- strategic and customer docs:
|
||||||
|
- Kolkata builder intel
|
||||||
|
- Customer 0 strategy
|
||||||
|
- customer persona notes
|
||||||
|
- sprint and user story docs
|
||||||
|
|
||||||
|
Excluded from synthesis:
|
||||||
|
|
||||||
|
- any path containing `Sourik`
|
||||||
|
- vendor or dependency markdown such as `node_modules`
|
||||||
|
- non-authoritative third-party readmes unless they directly influenced repo behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Product Thesis
|
||||||
|
|
||||||
|
Velocity is a real-estate sales acceleration platform for high-value property selling environments.
|
||||||
|
|
||||||
|
Its core promise is:
|
||||||
|
|
||||||
|
- compress sales cycles
|
||||||
|
- increase intelligence during sales interactions
|
||||||
|
- reduce lead leakage
|
||||||
|
- preserve data sovereignty
|
||||||
|
- give each property or portfolio a private, premium operating stack
|
||||||
|
|
||||||
|
The product is built around a first-principles model:
|
||||||
|
|
||||||
|
- property-specific intelligence matters
|
||||||
|
- portfolio intelligence unlocks when multiple properties are active
|
||||||
|
- AI should operate the workflow surface, not just answer questions
|
||||||
|
- product delivery must be standardized enough to avoid bespoke installation chaos
|
||||||
|
|
||||||
|
Velocity is therefore best understood as a modular operating system, not a single dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Commercial Model
|
||||||
|
|
||||||
|
The business model reflected in the documentation is:
|
||||||
|
|
||||||
|
- initial setup fee
|
||||||
|
- monthly maintenance / bleeding-edge upgrade fee
|
||||||
|
- inventory-linked or performance-linked downstream value capture
|
||||||
|
|
||||||
|
The product is not sold primarily by seats. The most important commercial unit is the property, with portfolio behavior unlocking when multiple properties are onboarded.
|
||||||
|
|
||||||
|
Working internal segmentation:
|
||||||
|
|
||||||
|
- **Tier 3 / city-channel model**
|
||||||
|
- CP or channel-heavy deployment
|
||||||
|
- one city per install
|
||||||
|
- can cover many builders in that city
|
||||||
|
- user count can still be high
|
||||||
|
- **Tier 2 / project-builder model**
|
||||||
|
- per-project deployment for a builder
|
||||||
|
- narrower operational scope
|
||||||
|
- property-specific generation and sales workflows
|
||||||
|
- **Tier 1 / enterprise portfolio model**
|
||||||
|
- multi-property or multi-portfolio controls
|
||||||
|
- monitoring and interaction layers across properties
|
||||||
|
- governance and deeper integrations
|
||||||
|
|
||||||
|
Commercially, the strongest internal framing is:
|
||||||
|
|
||||||
|
- first property gets full setup
|
||||||
|
- second property unlocks portfolio features
|
||||||
|
- enterprise control grows with property count and operational complexity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Customer and User Model
|
||||||
|
|
||||||
|
### Customer Types
|
||||||
|
|
||||||
|
- developers / builders
|
||||||
|
- brokerages and CPs
|
||||||
|
- city-channel sales operators
|
||||||
|
- enterprise portfolio operators
|
||||||
|
|
||||||
|
### Internal User Types
|
||||||
|
|
||||||
|
- junior broker
|
||||||
|
- senior broker
|
||||||
|
- sales director
|
||||||
|
- marketing operator
|
||||||
|
- data steward
|
||||||
|
- compliance reviewer
|
||||||
|
- platform admin
|
||||||
|
- operator / infra admin
|
||||||
|
|
||||||
|
### Team Reality
|
||||||
|
|
||||||
|
The current docs imply two simultaneous realities:
|
||||||
|
|
||||||
|
- product reality for future customers
|
||||||
|
- internal Desineuron operating reality used to build, demo, and run the system
|
||||||
|
|
||||||
|
This bibel captures both, but prefers the operational truth when implementation details matter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Suite Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Velocity User Surface] --> B[Web App]
|
||||||
|
A --> C[iPad App]
|
||||||
|
B --> D[FastAPI Neural Core]
|
||||||
|
C --> D
|
||||||
|
D --> E[Oracle]
|
||||||
|
D --> F[Sentinel]
|
||||||
|
D --> G[Inventory]
|
||||||
|
D --> H[Dream Weaver / Catalyst]
|
||||||
|
E --> I[PostgreSQL]
|
||||||
|
F --> I
|
||||||
|
G --> I
|
||||||
|
D --> J[NemoClaw / Reasoning Layer]
|
||||||
|
H --> K[ComfyUI / GPU Workloads]
|
||||||
|
L[Linux Control Surface] --> M[AWS GPU Nodes]
|
||||||
|
L --> N[S3 Canonical Asset Store]
|
||||||
|
O[t4g Stable Ingress] --> B
|
||||||
|
O --> C
|
||||||
|
O --> L
|
||||||
|
O --> M
|
||||||
|
```
|
||||||
|
|
||||||
|
Velocity has four major planes:
|
||||||
|
|
||||||
|
- **experience plane**
|
||||||
|
- web app
|
||||||
|
- iPad app
|
||||||
|
- **intelligence plane**
|
||||||
|
- Oracle
|
||||||
|
- Sentinel
|
||||||
|
- NemoClaw
|
||||||
|
- Dream Weaver / Catalyst
|
||||||
|
- **data plane**
|
||||||
|
- PostgreSQL
|
||||||
|
- asset storage
|
||||||
|
- S3 model store
|
||||||
|
- **operations plane**
|
||||||
|
- Linux control surface
|
||||||
|
- stable ingress
|
||||||
|
- AWS GPU workers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Product Surfaces
|
||||||
|
|
||||||
|
The currently documented product surfaces are:
|
||||||
|
|
||||||
|
- **Dashboard**
|
||||||
|
- KPIs, health, activity, sentiment, velocity
|
||||||
|
- **Oracle**
|
||||||
|
- AI CRM and analytical canvas
|
||||||
|
- **Sentinel**
|
||||||
|
- biometric and attention-scoring engine
|
||||||
|
- **Inventory**
|
||||||
|
- unit and property availability tracking
|
||||||
|
- **Dream Weaver / Catalyst**
|
||||||
|
- visual generation and asset transformation
|
||||||
|
- **Settings / operator surfaces**
|
||||||
|
- system configuration, connectivity, route control, infra control
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Oracle
|
||||||
|
|
||||||
|
Oracle is the AI operating surface of Velocity. It is not a simple chatbot.
|
||||||
|
|
||||||
|
### What Oracle Is
|
||||||
|
|
||||||
|
- a prompt-driven CRM intelligence surface
|
||||||
|
- a persistent vertical canvas
|
||||||
|
- a branchable and mergeable analytical workspace
|
||||||
|
- a controlled data access gateway mediated by Nemoclaw and policy
|
||||||
|
|
||||||
|
### Core Oracle Principles
|
||||||
|
|
||||||
|
- natural-language intent becomes typed execution
|
||||||
|
- durable page revisions replace ephemeral AI replies
|
||||||
|
- collaboration uses forks and merge requests
|
||||||
|
- provenance and auditability are mandatory
|
||||||
|
- AI planning must remain policy-constrained
|
||||||
|
|
||||||
|
### Oracle Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[User Prompt] --> B[Prompt Execution]
|
||||||
|
B --> C[NemoClaw Planning]
|
||||||
|
C --> D[Policy Validation]
|
||||||
|
D --> E[Data Access Gateway]
|
||||||
|
E --> F[PostgreSQL / Tenant Data]
|
||||||
|
F --> G[Visualization Resolver]
|
||||||
|
G --> H[Canvas Component]
|
||||||
|
H --> I[Revisioned Oracle Canvas]
|
||||||
|
I --> J[Fork / Merge Workflow]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Oracle Behaviors
|
||||||
|
|
||||||
|
- prompt authoring with context
|
||||||
|
- retrieval planning through semantic mapping
|
||||||
|
- policy-validated data access
|
||||||
|
- component synthesis or template selection
|
||||||
|
- append/insert/replace on a vertically scrolling canvas
|
||||||
|
- fork and merge semantics for shared work
|
||||||
|
|
||||||
|
### Oracle Truth in Repo
|
||||||
|
|
||||||
|
The repo has:
|
||||||
|
|
||||||
|
- a styled Oracle shell
|
||||||
|
- temporary mock-oriented UI behavior
|
||||||
|
- placeholder backend Oracle route files
|
||||||
|
|
||||||
|
The repo does not yet have:
|
||||||
|
|
||||||
|
- the full production Oracle runtime mounted and wired end to end
|
||||||
|
|
||||||
|
So Oracle is conceptually mature, but partially implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sentinel
|
||||||
|
|
||||||
|
Sentinel is the biometric, session-intelligence, and attention-scoring engine.
|
||||||
|
|
||||||
|
### Core Principles
|
||||||
|
|
||||||
|
- local-first perception
|
||||||
|
- no raw webcam stream to remote backend
|
||||||
|
- PostgreSQL as source of truth
|
||||||
|
- real-time broker-facing intelligence
|
||||||
|
- infrastructure truth over nostalgic planning
|
||||||
|
|
||||||
|
### Sentinel Inputs
|
||||||
|
|
||||||
|
- browser-side facial blendshape data
|
||||||
|
- scene timing
|
||||||
|
- CRM context
|
||||||
|
- asset opens
|
||||||
|
- CCTV enrichment
|
||||||
|
- auto-mode session evidence
|
||||||
|
|
||||||
|
### Sentinel Outputs
|
||||||
|
|
||||||
|
- QD score
|
||||||
|
- lead tags
|
||||||
|
- live notifications
|
||||||
|
- session intelligence
|
||||||
|
- vault-open intelligence
|
||||||
|
- auto-mode lead linkage
|
||||||
|
|
||||||
|
### Sentinel Modes
|
||||||
|
|
||||||
|
- **Assigned Mode**
|
||||||
|
- tied to existing lead
|
||||||
|
- **Auto Mode**
|
||||||
|
- no pre-bound lead
|
||||||
|
- CCTV and session evidence reconcile later
|
||||||
|
|
||||||
|
### Sentinel Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Browser Webcam + MediaPipe] --> B[Biometric Packets]
|
||||||
|
B --> C[/api/sentinel/ws/perception]
|
||||||
|
C --> D[Scene Lookup]
|
||||||
|
D --> E[NemoClaw Scoring]
|
||||||
|
E --> F[perception_sessions]
|
||||||
|
E --> G[omnichannel_logs]
|
||||||
|
E --> H[Live QD Broadcast]
|
||||||
|
I[CCTV OCR/Event] --> J[/api/cctv/event]
|
||||||
|
J --> K[Auto Mode Matcher]
|
||||||
|
K --> L[Lead Link or Lead Create]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Active Truth
|
||||||
|
|
||||||
|
The docs are explicit that:
|
||||||
|
|
||||||
|
- NVIDIA-hosted completions are the active primary path
|
||||||
|
- Ollama/OpenShell exist but are not the production-first scoring path
|
||||||
|
|
||||||
|
That distinction should remain explicit in team communication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Dream Weaver and Catalyst
|
||||||
|
|
||||||
|
Dream Weaver is the structural-preservation visual restyling system. Catalyst is the broader generation and asset automation layer.
|
||||||
|
|
||||||
|
### Dream Weaver Objective
|
||||||
|
|
||||||
|
Restyle interiors while preserving:
|
||||||
|
|
||||||
|
- geometry
|
||||||
|
- window placement
|
||||||
|
- vanishing points
|
||||||
|
- architectural truth
|
||||||
|
|
||||||
|
### Technical Strategy
|
||||||
|
|
||||||
|
- RealVisXL V5.0 Lightning as core model family in the historical design docs
|
||||||
|
- stacked ControlNet approach
|
||||||
|
- depth + line preservation
|
||||||
|
- SAM-based masking
|
||||||
|
- workflow portability through ComfyUI API JSON
|
||||||
|
|
||||||
|
### Operational Direction
|
||||||
|
|
||||||
|
The later infra work also shows Qwen-based image edit/generation workflows entering the stack. So the project now contains two overlapping visual-generation realities:
|
||||||
|
|
||||||
|
- Dream Weaver spec centered on SDXL / RealVisXL / ControlNet / SAM
|
||||||
|
- operational AWS generation work centered on Qwen image models and ComfyUI / GPU orchestration
|
||||||
|
|
||||||
|
These should be treated as parallel capabilities, not contradictions.
|
||||||
|
|
||||||
|
### Dream Weaver Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[Reference / Room Image] --> B[Preprocess]
|
||||||
|
B --> C[M-LSD]
|
||||||
|
B --> D[Depth]
|
||||||
|
B --> E[SAM / Masking]
|
||||||
|
C --> F[Control Stack]
|
||||||
|
D --> F
|
||||||
|
E --> F
|
||||||
|
F --> G[Sampler / Restyler]
|
||||||
|
G --> H[Styled Output]
|
||||||
|
H --> I[API / iPad / Batch Workflow]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operational Purpose
|
||||||
|
|
||||||
|
- interior restyling
|
||||||
|
- marketing posters
|
||||||
|
- cinematic video
|
||||||
|
- mobile-triggered asset generation
|
||||||
|
- broker-facing wow factor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Web Application
|
||||||
|
|
||||||
|
The web app is a React + TypeScript + Vite application.
|
||||||
|
|
||||||
|
### Frontend Stack
|
||||||
|
|
||||||
|
- React 19
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- Zustand
|
||||||
|
- Tailwind CSS
|
||||||
|
- shadcn/ui
|
||||||
|
- Radix UI
|
||||||
|
- Framer Motion
|
||||||
|
- Recharts
|
||||||
|
- React Hook Form
|
||||||
|
- Zod
|
||||||
|
|
||||||
|
### Core Web Modules
|
||||||
|
|
||||||
|
- Dashboard
|
||||||
|
- Oracle
|
||||||
|
- Sentinel
|
||||||
|
- Inventory
|
||||||
|
- Settings
|
||||||
|
|
||||||
|
### State Model
|
||||||
|
|
||||||
|
The docs describe Zustand-backed state slices for:
|
||||||
|
|
||||||
|
- auth
|
||||||
|
- navigation
|
||||||
|
- Oracle
|
||||||
|
- Sentinel
|
||||||
|
- Dashboard
|
||||||
|
- Inventory
|
||||||
|
- system state
|
||||||
|
|
||||||
|
### Current Truth
|
||||||
|
|
||||||
|
The web app appears stronger as a polished shell than as a fully integrated production data surface. It carries much of the premium experience language and component structure the rest of the system is meant to converge toward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. iOS Application
|
||||||
|
|
||||||
|
Velocity iOS is the native mobile counterpart for Apple devices, especially the showroom iPad surface.
|
||||||
|
|
||||||
|
### iOS Stack
|
||||||
|
|
||||||
|
- Swift
|
||||||
|
- SwiftUI
|
||||||
|
- Combine / Observation
|
||||||
|
- Alamofire for networking
|
||||||
|
- native animation and glassmorphism
|
||||||
|
|
||||||
|
### iOS Modules
|
||||||
|
|
||||||
|
- Dashboard
|
||||||
|
- Oracle
|
||||||
|
- Sentinel
|
||||||
|
- Inventory
|
||||||
|
- Settings
|
||||||
|
|
||||||
|
### Strategic Role
|
||||||
|
|
||||||
|
The iPad app is not just a companion. It is central to the product’s premium physical-sales-gallery positioning.
|
||||||
|
|
||||||
|
Important documented capabilities:
|
||||||
|
|
||||||
|
- portable sales intelligence
|
||||||
|
- native presentation surface
|
||||||
|
- AI generation triggering
|
||||||
|
- AR sun-path overlay
|
||||||
|
- inventory and lead interaction on the floor
|
||||||
|
|
||||||
|
### Design Direction
|
||||||
|
|
||||||
|
- native fluidity
|
||||||
|
- battery efficiency
|
||||||
|
- premium dark/glass aesthetic
|
||||||
|
- parity with web where needed, but not web-in-a-wrapper
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Backend and AI Runtime
|
||||||
|
|
||||||
|
The backend is a FastAPI neural core with PostgreSQL, auth, websockets, and AI orchestration.
|
||||||
|
|
||||||
|
### Key Responsibilities
|
||||||
|
|
||||||
|
- API routing
|
||||||
|
- auth and RBAC
|
||||||
|
- CRM and lead operations
|
||||||
|
- Sentinel perception processing
|
||||||
|
- Oracle execution path
|
||||||
|
- websockets and notifications
|
||||||
|
- video and scene catalog access
|
||||||
|
- NemoClaw integration
|
||||||
|
|
||||||
|
### NemoClaw Role
|
||||||
|
|
||||||
|
NemoClaw is the reasoning layer used across:
|
||||||
|
|
||||||
|
- lead tagging
|
||||||
|
- CCTV profiling
|
||||||
|
- QD scoring
|
||||||
|
- future Oracle planning and data access interpretation
|
||||||
|
|
||||||
|
### Prompt Truth in Repo
|
||||||
|
|
||||||
|
The repo contains explicit prompt artifacts for:
|
||||||
|
|
||||||
|
- `cctv_profiler`
|
||||||
|
- `lead_tagger`
|
||||||
|
- `qd_calculator`
|
||||||
|
|
||||||
|
These are concrete prompt contracts, not vague intentions. They should be treated as product logic.
|
||||||
|
|
||||||
|
### Runtime Reality
|
||||||
|
|
||||||
|
The system documentation repeatedly shows a hybrid runtime model:
|
||||||
|
|
||||||
|
- hosted NVIDIA-compatible reasoning path as primary
|
||||||
|
- private / local alternatives possible
|
||||||
|
- runtime abstraction should outlive vendor choice
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Data Model and Core Entities
|
||||||
|
|
||||||
|
The major persistent entities reflected across the docs are:
|
||||||
|
|
||||||
|
- users and roles
|
||||||
|
- leads and lead intelligence
|
||||||
|
- inventory and project data
|
||||||
|
- perception sessions
|
||||||
|
- CCTV events
|
||||||
|
- vault assets and opens
|
||||||
|
- omnichannel logs
|
||||||
|
- page revisions and canvas components
|
||||||
|
- model catalog and hydration state
|
||||||
|
- machine sessions and cost records
|
||||||
|
|
||||||
|
### Persistent Truth
|
||||||
|
|
||||||
|
PostgreSQL is the primary source of truth for operational data.
|
||||||
|
S3 is becoming the canonical store for large model and asset artifacts.
|
||||||
|
AWS GPU NVMe is treated as fast ephemeral runtime cache, not authoritative storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Infrastructure and Deployment Model
|
||||||
|
|
||||||
|
Project Velocity now has a clear split between:
|
||||||
|
|
||||||
|
- **stable control surfaces**
|
||||||
|
- **ephemeral compute workers**
|
||||||
|
- **canonical storage**
|
||||||
|
|
||||||
|
### Current Deployment Pattern
|
||||||
|
|
||||||
|
- Linux box:
|
||||||
|
- long-lived control surface
|
||||||
|
- private origin services
|
||||||
|
- operator tooling
|
||||||
|
- AWS t4g ingress:
|
||||||
|
- stable public edge
|
||||||
|
- route manager
|
||||||
|
- AWS GPU instances:
|
||||||
|
- disposable compute nodes
|
||||||
|
- ComfyUI and model workloads
|
||||||
|
- S3:
|
||||||
|
- canonical model / workflow / asset store
|
||||||
|
|
||||||
|
### Architectural Principle
|
||||||
|
|
||||||
|
This is not “one giant computer.”
|
||||||
|
It is:
|
||||||
|
|
||||||
|
- one control plane
|
||||||
|
- one ingress plane
|
||||||
|
- one durable asset plane
|
||||||
|
- many disposable compute workers
|
||||||
|
|
||||||
|
That is the correct model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Stable Ingress Layer
|
||||||
|
|
||||||
|
The stable ingress layer is the permanent public front door.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
- stable public IP
|
||||||
|
- DNS target for public routes
|
||||||
|
- TLS termination
|
||||||
|
- hostname-based routing
|
||||||
|
- forwarding to Linux or AWS backends
|
||||||
|
|
||||||
|
### Current Public Surfaces
|
||||||
|
|
||||||
|
The handoff documents establish these public hostnames as part of the route model:
|
||||||
|
|
||||||
|
- `office.desineuron.in`
|
||||||
|
- `git.desineuron.in`
|
||||||
|
- `cloud.desineuron.in`
|
||||||
|
- `projects.desineuron.in`
|
||||||
|
- `talk.desineuron.in`
|
||||||
|
- `vpn.desineuron.in`
|
||||||
|
- `comfy.desineuron.in`
|
||||||
|
- `ops.desineuron.in`
|
||||||
|
|
||||||
|
### Key Design Principle
|
||||||
|
|
||||||
|
Backends may change. Public identity should not.
|
||||||
|
|
||||||
|
That is why:
|
||||||
|
|
||||||
|
- GPU workers should not own the stable Elastic IP
|
||||||
|
- the ingress box should stay alive and route based on managed config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Linux AWS Control Surface
|
||||||
|
|
||||||
|
The Linux box is now the operational control surface for AWS.
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
|
||||||
|
- machine lifecycle management
|
||||||
|
- preferred instance selection
|
||||||
|
- spot/on-demand visibility
|
||||||
|
- session and cost tracking
|
||||||
|
- route management through ingress
|
||||||
|
- model ingest and hydration orchestration
|
||||||
|
- operator UI and CLI
|
||||||
|
|
||||||
|
### Canonical Asset Strategy
|
||||||
|
|
||||||
|
The documentation evolution shows a strong convergence:
|
||||||
|
|
||||||
|
- Linux can store local copies
|
||||||
|
- but S3 should be the canonical large-model source for ephemeral AWS hydration
|
||||||
|
|
||||||
|
That is the right direction because S3 is:
|
||||||
|
|
||||||
|
- durable
|
||||||
|
- AWS-native
|
||||||
|
- fast to hydrate with `s5cmd`
|
||||||
|
- not dependent on home network conditions
|
||||||
|
|
||||||
|
### Control Plane Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Operator UI / CLI on Linux] --> B[Launch AWS Worker]
|
||||||
|
B --> C[Worker Bootstrap]
|
||||||
|
C --> D[Hydrate from S3]
|
||||||
|
D --> E[Verify Manifest]
|
||||||
|
E --> F[Start Workload]
|
||||||
|
F --> G[Map Route Through Ingress]
|
||||||
|
G --> H[Track Runtime / Cost / Logs]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Operating Flows
|
||||||
|
|
||||||
|
### Lead Intelligence Flow
|
||||||
|
|
||||||
|
- lead enters through channel
|
||||||
|
- Oracle / backend receives it
|
||||||
|
- tagging and enrichment happen
|
||||||
|
- lead lands in CRM operating surface
|
||||||
|
|
||||||
|
### Sentinel Session Flow
|
||||||
|
|
||||||
|
- broker runs session
|
||||||
|
- MediaPipe emits perception packets
|
||||||
|
- backend computes QD and updates lead/session state
|
||||||
|
- notifications stream back in real time
|
||||||
|
|
||||||
|
### Dream Weaver / Comfy Flow
|
||||||
|
|
||||||
|
- operator or app triggers generation
|
||||||
|
- GPU worker receives workflow
|
||||||
|
- model hydrates if missing
|
||||||
|
- ComfyUI or pipeline runs
|
||||||
|
- output is returned or archived
|
||||||
|
|
||||||
|
### Infra Control Flow
|
||||||
|
|
||||||
|
- operator selects profile
|
||||||
|
- Linux control surface launches worker
|
||||||
|
- route gets mapped through ingress
|
||||||
|
- workload is exposed without changing public identity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. Security, Privacy, and Sovereignty
|
||||||
|
|
||||||
|
Project Velocity’s strongest recurring architectural doctrine is data sovereignty.
|
||||||
|
|
||||||
|
### Security Principles
|
||||||
|
|
||||||
|
- client data belongs to client
|
||||||
|
- on-prem or client-controlled cloud must be supported
|
||||||
|
- zero-trust between services
|
||||||
|
- least privilege for machine roles
|
||||||
|
- no hidden production dependencies on developer laptops
|
||||||
|
- auditability matters for both operations and business decisions
|
||||||
|
|
||||||
|
### Privacy Positioning
|
||||||
|
|
||||||
|
Velocity is explicitly positioned as anti-SaaS in the strategic docs.
|
||||||
|
|
||||||
|
That means product delivery should support:
|
||||||
|
|
||||||
|
- on-prem deployment
|
||||||
|
- client-owned cloud
|
||||||
|
- local or sovereign runtime options
|
||||||
|
- explicit consent and audit around biometric flows
|
||||||
|
|
||||||
|
This is not just a compliance detail. It is a core selling argument.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. Current Live Truth
|
||||||
|
|
||||||
|
Based on the most recent infrastructure handoff and operational truth docs, the current live picture is:
|
||||||
|
|
||||||
|
- stable ingress is running on AWS `t4g.micro`
|
||||||
|
- Linux box is the long-lived origin and control surface
|
||||||
|
- ComfyUI is exposed through managed ingress routing
|
||||||
|
- auto-heal exists for:
|
||||||
|
- home IP sync
|
||||||
|
- Comfy route sync
|
||||||
|
- ops control plane is live on Linux
|
||||||
|
- S3 is in place as the canonical control-plane bucket
|
||||||
|
|
||||||
|
Important operational truths that should not be lost:
|
||||||
|
|
||||||
|
- GPU nodes are ephemeral and should be treated that way
|
||||||
|
- route management should be dynamic, not hardcoded
|
||||||
|
- NVMe is for speed, not truth
|
||||||
|
- Linux and S3 together are the operational backbone
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 21. Build Priorities and Open Gaps
|
||||||
|
|
||||||
|
The documentation implies this priority order:
|
||||||
|
|
||||||
|
1. stabilize control plane and ingress
|
||||||
|
2. stabilize model hydration and GPU orchestration
|
||||||
|
3. finish Oracle backend/runtime implementation
|
||||||
|
4. continue iOS and web convergence
|
||||||
|
5. standardize packaging for on-prem delivery
|
||||||
|
|
||||||
|
### Open Gaps
|
||||||
|
|
||||||
|
- Oracle production runtime is not fully wired yet
|
||||||
|
- several frontend surfaces still rely on mock or transitional behavior
|
||||||
|
- deployment packaging for customer installs is not yet standardized enough
|
||||||
|
- multi-property / portfolio product packaging needs formalization in product artifacts
|
||||||
|
- some business docs are strong on thesis but still need conversion into installable product contracts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 22. Runbooks and Team Usage
|
||||||
|
|
||||||
|
### What the team should point to for truth
|
||||||
|
|
||||||
|
- **this master bibel** for unified product and architecture truth
|
||||||
|
- **ingress handoff** for current infra routing truth
|
||||||
|
- **ops control plane bibel** for operator workflows
|
||||||
|
|
||||||
|
### When a new service is added
|
||||||
|
|
||||||
|
The team should define:
|
||||||
|
|
||||||
|
- what plane it belongs to
|
||||||
|
- whether it is stable or ephemeral
|
||||||
|
- where its truth is stored
|
||||||
|
- how it is routed publicly
|
||||||
|
- which system owns its startup, health, and recovery
|
||||||
|
|
||||||
|
### Operational Rule
|
||||||
|
|
||||||
|
If a workflow depends on a developer’s Windows laptop staying alive, it is not production-ready.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 23. Source Lineage Map
|
||||||
|
|
||||||
|
This master bibel draws primarily from these source families:
|
||||||
|
|
||||||
|
### Product and architecture
|
||||||
|
|
||||||
|
- `Project Velocity - The Oracle.md`
|
||||||
|
- `The Sentinel Bibel.md`
|
||||||
|
- `velocity_technical_bible.md`
|
||||||
|
- `velocity_ios_bible.md`
|
||||||
|
- `oracle_development_status.md`
|
||||||
|
|
||||||
|
### Visual generation
|
||||||
|
|
||||||
|
- `DREAMWEAVER_TECHNICAL_SPEC.md`
|
||||||
|
- `A100_DEPLOYMENT_VALIDATION.md`
|
||||||
|
- `comfy_engine/scripts/README.md`
|
||||||
|
- `Project Velocity_ Dream Weaver.md`
|
||||||
|
|
||||||
|
### Infra and operations
|
||||||
|
|
||||||
|
- `TEAM_HANDOFF_2026-04-08.md`
|
||||||
|
- `Desineuron Ops Control Plane Bibel.md`
|
||||||
|
- `nemoclaw_setup_truth.md`
|
||||||
|
- `infrastructure_deployment_manifest.md`
|
||||||
|
- ingress and ops READMEs
|
||||||
|
|
||||||
|
### Business and GTM context
|
||||||
|
|
||||||
|
- `Velocity Kolkata Customer 0 Strategy - Get My Ghar.md`
|
||||||
|
- `Kolkata Builder Intel and Meeting Map - April 2026.md`
|
||||||
|
- `Customer Personas for Abu Dhabi and Dubai...md`
|
||||||
|
- sprint user story docs
|
||||||
|
|
||||||
|
### Prompt and AI behavior contracts
|
||||||
|
|
||||||
|
- `backend/nemoclaw_prompts/*.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Position
|
||||||
|
|
||||||
|
Project Velocity is best understood as a sovereign, modular real-estate operating system with:
|
||||||
|
|
||||||
|
- premium user surfaces
|
||||||
|
- AI-driven CRM and perception intelligence
|
||||||
|
- property-linked generation workflows
|
||||||
|
- a private operational backbone
|
||||||
|
- a route-stable ingress layer
|
||||||
|
- a Linux-hosted control plane
|
||||||
|
- S3-backed canonical model and asset strategy
|
||||||
|
|
||||||
|
That is the coherent architecture the scattered docs were already converging toward.
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
# Sourik Code Intake and Compatibility Report
|
||||||
|
|
||||||
|
**Prepared:** 2026-04-12
|
||||||
|
**Scope:** `Project_Velocity/Sourik` only, evaluated against the current non-`Sourik` Project Velocity codebase, built features, and live operating model
|
||||||
|
**Purpose:** Decide what from `Sourik` is worth merging, what is redundant, what is bloat, and where the conflicts will be
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
| Score | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 | Banish. Do not merge into main. |
|
||||||
|
| 2 | Archive only. Historical value, no production merge value. |
|
||||||
|
| 3 | Very weak merge candidate. Requires heavy rewrite or extraction. |
|
||||||
|
| 4 | Risky or partial. Import only if a specific owner adopts it. |
|
||||||
|
| 5 | Mixed value. Keep for reference, not direct merge. |
|
||||||
|
| 6 | Useful implementation ideas, but not merge-ready. |
|
||||||
|
| 7 | Good candidate for selective integration. |
|
||||||
|
| 8 | Strong subsystem candidate. |
|
||||||
|
| 9 | High-value, near-merge-ready if wiring assumptions are corrected. |
|
||||||
|
| 10 | Critical import candidate. No direct equivalent or materially better than current mainline. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The `Sourik` tree is not one clean feature branch. It is a mixed bundle of:
|
||||||
|
|
||||||
|
- real backend/API work
|
||||||
|
- a separate Go agent runtime
|
||||||
|
- frontend additions for marketing and analytics
|
||||||
|
- a partial iOS spike
|
||||||
|
- strong test evidence in some areas
|
||||||
|
- large amounts of residue:
|
||||||
|
- local env files
|
||||||
|
- coverage outputs
|
||||||
|
- compiled binaries
|
||||||
|
- sqlite prototypes
|
||||||
|
- pycache
|
||||||
|
- logs
|
||||||
|
- duplicate docs
|
||||||
|
|
||||||
|
The correct merge posture is:
|
||||||
|
|
||||||
|
- **do not merge the tree wholesale**
|
||||||
|
- **extract selected subsystems**
|
||||||
|
- **banish operational residue**
|
||||||
|
- **treat the Go runtime as an alternate architecture, not as an automatic addition**
|
||||||
|
|
||||||
|
Highest-value areas in `Sourik`:
|
||||||
|
|
||||||
|
- Python API modules around leads, persona, analytics, websocket, kanban, and Sentinel consent/GDPR patterns
|
||||||
|
- test coverage and test cases
|
||||||
|
- some Catalyst service abstractions
|
||||||
|
- some iOS camera/time-light experiment code
|
||||||
|
|
||||||
|
Highest-risk areas:
|
||||||
|
|
||||||
|
- duplicate backend entrypoint and alternate runtime model
|
||||||
|
- alternate Nemoclaw + MCP + Go server stack
|
||||||
|
- hardcoded stale infrastructure assumptions
|
||||||
|
- local SQLite-first assumptions that conflict with current PostgreSQL truth
|
||||||
|
- Comfy service hardcoded to stale IPs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Sourik Actually Contains
|
||||||
|
|
||||||
|
### Implementation Shape
|
||||||
|
|
||||||
|
`Sourik/velocity` contains:
|
||||||
|
|
||||||
|
- Python FastAPI app:
|
||||||
|
- `main.py`
|
||||||
|
- `api/*.py`
|
||||||
|
- `services/*.py`
|
||||||
|
- `db/connection.py`
|
||||||
|
- Go runtime:
|
||||||
|
- `main.go`
|
||||||
|
- `internal/nemoclaw/*`
|
||||||
|
- `internal/oracle/*`
|
||||||
|
- `internal/mcp/*`
|
||||||
|
- `internal/sentinel/*`
|
||||||
|
- `integrations/*.go`
|
||||||
|
- frontend additions:
|
||||||
|
- `frontend/src/app/marketing/page.tsx`
|
||||||
|
- `frontend/src/lib/api.ts`
|
||||||
|
- dashboard/marketing components
|
||||||
|
- iOS spike:
|
||||||
|
- `VelocityApp/*`
|
||||||
|
- docs and summaries:
|
||||||
|
- `README.md`
|
||||||
|
- `SPRINT_SUMMARY_FOR_SAGNIK.md`
|
||||||
|
- `SPEC.md`
|
||||||
|
- `docs/*`
|
||||||
|
- residue:
|
||||||
|
- `.env`
|
||||||
|
- `prototype.db`
|
||||||
|
- coverage outputs
|
||||||
|
- binaries
|
||||||
|
- logs
|
||||||
|
- pycache
|
||||||
|
|
||||||
|
### Sourik Architecture According to Sourik
|
||||||
|
|
||||||
|
The root `README.md` describes an “Agentic Operations Layer” with:
|
||||||
|
|
||||||
|
- WhatsApp webhook
|
||||||
|
- Go OpenClaw/NemoClaw layer
|
||||||
|
- MCP server
|
||||||
|
- ComfyUI / CRM / visual hub integrations
|
||||||
|
|
||||||
|
That is a materially different center-of-gravity from the current mainline, where:
|
||||||
|
|
||||||
|
- FastAPI backend is the operational center
|
||||||
|
- Linux control surface and ingress are already live
|
||||||
|
- Comfy routing and AWS orchestration are already established
|
||||||
|
|
||||||
|
This matters. `Sourik` is not just “new features.” It contains an alternate operating model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility with Current Mainline
|
||||||
|
|
||||||
|
### Current Mainline Truth
|
||||||
|
|
||||||
|
The non-`Sourik` codebase already has:
|
||||||
|
|
||||||
|
- a live FastAPI backend in `backend/main.py`
|
||||||
|
- active routers for Sentinel, CCTV, videos, scenes, vault
|
||||||
|
- a real Nemoclaw client abstraction in Python
|
||||||
|
- an Oracle implementation path in `backend/oracle/*`
|
||||||
|
- a premium React app in `app/src/*`
|
||||||
|
- a live ingress plane in `infrastructure/desineuron_ingress/*`
|
||||||
|
- a live Linux AWS control surface in `infrastructure/ops_control_plane/*`
|
||||||
|
- a real iOS app tree in `iOS/velocity/velocity/*`
|
||||||
|
|
||||||
|
So the comparison baseline is not theoretical. It is a partially working system with live infrastructure.
|
||||||
|
|
||||||
|
### Main Compatibility Findings
|
||||||
|
|
||||||
|
| Sourik Area | Mainline Equivalent | Compatibility |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `velocity/api/leads.py` | no equally complete current leads CRUD module in mainline backend | Good candidate |
|
||||||
|
| `velocity/api/sentinel.py` | overlaps with `backend/routers/sentinel.py` but focuses on consent/GDPR/biometric storage | Selective candidate |
|
||||||
|
| `velocity/api/catalyst.py` | overlaps with `backend/api/routes_catalyst.py` | Merge conflict likely |
|
||||||
|
| `velocity/api/ws.py` | overlaps conceptually with current websocket/event logic | Selective candidate |
|
||||||
|
| `velocity/api/analytics.py` | weak/no exact current equivalent | Good candidate |
|
||||||
|
| `velocity/api/persona.py` | likely additive | Good candidate |
|
||||||
|
| `velocity/api/kanban.py` | no clear current equivalent in mainline | Good candidate |
|
||||||
|
| `velocity/services/comfyui_service.py` | conflicts with current ingress + Comfy routing + live GPU architecture | Poor direct candidate |
|
||||||
|
| `velocity/main.py` | conflicts with `backend/main.py` | Do not merge directly |
|
||||||
|
| `velocity/main.go` + `internal/*` | alternate runtime architecture | Do not merge directly |
|
||||||
|
| `VelocityApp/*` | overlaps with current iOS app | Selective candidate only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scored Intake Table
|
||||||
|
|
||||||
|
| Sourik Path / Scope | Score | Merge Recommendation | Reason |
|
||||||
|
| --- | ---: | --- | --- |
|
||||||
|
| `Sourik/velocity/api/leads.py` | 8 | Selective merge candidate | Real CRUD, qualification, demographics endpoints. Mainline has product need here and no equally complete direct equivalent. Needs DB contract alignment. |
|
||||||
|
| `Sourik/velocity/api/kanban.py` | 8 | Selective merge candidate | Kanban behavior is useful and currently underrepresented in mainline implementation. |
|
||||||
|
| `Sourik/velocity/api/analytics.py` | 8 | Selective merge candidate | Adds reporting/analytics surface that appears useful for Marketing and Oracle support. |
|
||||||
|
| `Sourik/velocity/api/persona.py` | 7 | Selective merge candidate | Likely additive business logic and API value. |
|
||||||
|
| `Sourik/velocity/api/chat_logs.py` | 7 | Selective merge candidate | Useful if mapped into Oracle / CRM flows; needs contract alignment with current auth and DB. |
|
||||||
|
| `Sourik/velocity/api/ws.py` | 7 | Selective merge candidate | Useful patterns and tests likely exist, but websocket ownership already exists in current backend. |
|
||||||
|
| `Sourik/velocity/api/sentinel.py` | 7 | Extract specific features only | Valuable for biometric consent, GDPR, and export patterns. But it is SQLite-centered and overlaps with current Sentinel runtime. |
|
||||||
|
| `Sourik/velocity/tests/api/*` | 9 | Strong import candidate | Test suites are high-value, especially for a fragile future merge. Adapt tests even if code is not merged verbatim. |
|
||||||
|
| `Sourik/velocity/tests/internal/nemoclaw/*` | 7 | Import selectively | Good coverage and behavioral protection if Go/Nemo concepts are retained anywhere. |
|
||||||
|
| `Sourik/velocity/services/ad_network_skills.py` | 7 | Selective merge candidate | Likely useful for Catalyst ad automation and may complement current Meta-focused path. |
|
||||||
|
| `Sourik/velocity/services/social_posting.py` | 7 | Selective merge candidate | Useful capability if social publishing remains in scope. |
|
||||||
|
| `Sourik/velocity/services/catalyst_content.py` | 7 | Selective merge candidate | Useful Catalyst logic, but must be aligned with current Comfy and asset pipeline. |
|
||||||
|
| `Sourik/velocity/services/comfyui_service.py` | 4 | Reference only, do not merge directly | Hardcodes stale host/IP and assumes a different Comfy deployment pattern from the current live ingress-managed GPU model. |
|
||||||
|
| `Sourik/velocity/frontend/src/app/marketing/page.tsx` | 7 | Selective UI import candidate | Useful product surface not currently dominant in mainline app. Needs design-system integration and router alignment. |
|
||||||
|
| `Sourik/velocity/frontend/src/components/marketing/*` | 7 | Selective UI import candidate | Likely useful for Catalyst/marketing module. |
|
||||||
|
| `Sourik/velocity/frontend/src/components/dashboard/*` | 6 | Compare component by component | Useful ideas, but mainline already has strong dashboard surfaces. Merge only where functionality is genuinely additive. |
|
||||||
|
| `Sourik/velocity/frontend/src/lib/api.ts` | 5 | Reference only; do not merge as-is | Useful contract hints, but current mainline already has API client layers. This should inform consolidation, not become a second truth. |
|
||||||
|
| `Sourik/VelocityApp/Features/CameraView.swift` | 7 | Selective import candidate | Good feature spike: camera capture + send-to-Comfy flow + time/light controls. Worth comparing to current iOS inventory/AR path. |
|
||||||
|
| `Sourik/VelocityApp/Features/TimeControlSlider.swift` | 7 | Selective import candidate | Likely useful to augment Sun/lighting feature work. |
|
||||||
|
| `Sourik/VelocityApp/Features/TimeLightEngine.swift` | 7 | Selective import candidate | Strong fit with current Velocity iOS sun-path direction. |
|
||||||
|
| `Sourik/VelocityApp/Core/Router.swift` | 5 | Review only | Could help, but current iOS structure already has navigation patterns. |
|
||||||
|
| `Sourik/velocity/main.py` | 3 | Do not merge directly | It creates a second FastAPI root with a different DB model, routing shape, and operational assumptions. Extract modules only. |
|
||||||
|
| `Sourik/velocity/main.go` | 3 | Do not merge directly | Alternate runtime center. Valuable as concept or archived subsystem, but not compatible with current mainline architecture without a deliberate platform decision. |
|
||||||
|
| `Sourik/velocity/internal/nemoclaw/*` | 4 | Review as architecture experiments only | Significant conceptual overlap with current Python Nemoclaw path. A direct merge would create two control centers. |
|
||||||
|
| `Sourik/velocity/internal/mcp/*` | 5 | Review selectively | Interesting if you want future MCP work, but not aligned with current operational center yet. |
|
||||||
|
| `Sourik/velocity/internal/oracle/*` | 4 | Reference only for now | Current mainline already has an Oracle implementation direction in Python. Direct merge would fork the architecture. |
|
||||||
|
| `Sourik/velocity/internal/sentinel/sentinel_api.go` | 4 | Reference only | Sentinel is already active in Python backend. |
|
||||||
|
| `Sourik/velocity/integrations/*.go` | 5 | Review case-by-case | Potentially useful adapters, but only if the Go runtime is retained. |
|
||||||
|
| `Sourik/velocity/docs/COMFYUI_HANDOFF.md` | 6 | Keep as reference | Useful context for intended wire-up, but assumes VPN/headscale-era setup that no longer matches current ingress truth. |
|
||||||
|
| `Sourik/SPRINT_SUMMARY_FOR_SAGNIK.md` | 7 | Keep as reference artifact | Useful for intent, coverage claims, and blocked wire list. Not production code. |
|
||||||
|
| `Sourik/SPEC.md` | 5 | Archive as historical spec | Useful context, but contains stale node IPs, port assumptions, and stage-only instructions that no longer match live infrastructure. |
|
||||||
|
| `Sourik/PRD.md` | 2 | Archive only | Too thin to be operationally meaningful. |
|
||||||
|
| `Sourik/velocity/README.md` | 6 | Keep as architecture snapshot | Good summary of Sourik’s intended architecture. |
|
||||||
|
| `Sourik/velocity/.env` | 1 | Banish | Local secrets/config residue. Must not merge. |
|
||||||
|
| `Sourik/desineuron-l4-node.pem` | 1 | Banish | Private key material in source tree. Never merge. |
|
||||||
|
| `Sourik/velocity/db/prototype.db` | 1 | Banish | Local prototype database, not source. |
|
||||||
|
| `Sourik/velocity/prototype.db` | 1 | Banish | Same issue. |
|
||||||
|
| `Sourik/velocity/.coverage` | 1 | Banish | Coverage artifact. |
|
||||||
|
| `Sourik/velocity/coverage*` | 1 | Banish | Coverage outputs, not source. |
|
||||||
|
| `Sourik/velocity/.agent/*coverage*`, `*test_output*` | 1 | Banish | CI/test residue. |
|
||||||
|
| `Sourik/velocity/antigravity_server` | 1 | Banish | Compiled binary, not source. |
|
||||||
|
| `Sourik/velocity/antigravity_test_build.exe` | 1 | Banish | Compiled Windows artifact, not source. |
|
||||||
|
| `Sourik/velocity/__pycache__`, `.pytest_cache` | 1 | Banish | Generated caches. |
|
||||||
|
| `Sourik/velocity/logs/VALIDATION_FINAL.log` | 2 | Archive only | Useful as evidence, not source. |
|
||||||
|
| `Sourik/velocity/test_out.txt`, `guide_extracted.txt`, `guide_utf8.txt` | 2 | Archive only | Scratch outputs. |
|
||||||
|
| `Sourik/velocity/stubs/*` | 3 | Archive or keep in a clearly marked scratch area | Potentially useful examples, but not merge targets. |
|
||||||
|
| `Sourik/velocity/archive/*` | 2 | Archive only | Historical material. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bloat and Residue in Sourik
|
||||||
|
|
||||||
|
These are immediate no-merge items:
|
||||||
|
|
||||||
|
- private keys
|
||||||
|
- `.env`
|
||||||
|
- `.coverage`
|
||||||
|
- `coverage*`
|
||||||
|
- `.pytest_cache`
|
||||||
|
- `__pycache__`
|
||||||
|
- compiled binaries
|
||||||
|
- local sqlite databases
|
||||||
|
- raw logs
|
||||||
|
- scratch text files
|
||||||
|
|
||||||
|
These do not need debate. They are not source code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redundant vs Additive Features
|
||||||
|
|
||||||
|
### Redundant or Architecturally Conflicting
|
||||||
|
|
||||||
|
1. **FastAPI root app**
|
||||||
|
- `Sourik/velocity/main.py`
|
||||||
|
- conflicts with current `backend/main.py`
|
||||||
|
- creates a second root service with different routing and DB assumptions
|
||||||
|
|
||||||
|
2. **Go agent runtime**
|
||||||
|
- `Sourik/velocity/main.go`
|
||||||
|
- `internal/nemoclaw/*`
|
||||||
|
- `internal/mcp/*`
|
||||||
|
- this is an alternate operational center, not a drop-in feature set
|
||||||
|
|
||||||
|
3. **ComfyUI service**
|
||||||
|
- assumes stale IP-based deployment
|
||||||
|
- current system already has:
|
||||||
|
- stable ingress
|
||||||
|
- GPU worker tagging
|
||||||
|
- auto-healing route sync
|
||||||
|
- Linux AWS control plane
|
||||||
|
|
||||||
|
### Additive and Potentially Valuable
|
||||||
|
|
||||||
|
1. **Lead / Kanban / Persona / Analytics Python APIs**
|
||||||
|
- mainline can benefit directly from these
|
||||||
|
|
||||||
|
2. **Sentinel consent and GDPR patterns**
|
||||||
|
- these are useful as selective feature imports into current Sentinel
|
||||||
|
|
||||||
|
3. **Marketing frontend**
|
||||||
|
- a real additive surface for the Catalyst/marketing module
|
||||||
|
|
||||||
|
4. **iOS camera and time/light features**
|
||||||
|
- fit directly with current iPad vision and sun/AR direction
|
||||||
|
|
||||||
|
5. **Test suites**
|
||||||
|
- very high value regardless of whether code merges directly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility with Built Features
|
||||||
|
|
||||||
|
### Works with Current Mainline Direction
|
||||||
|
|
||||||
|
- property and lead workflows
|
||||||
|
- marketing/campaign intelligence
|
||||||
|
- camera-driven iOS interactions
|
||||||
|
- GDPR/consent handling
|
||||||
|
- Kanban CRM motions
|
||||||
|
|
||||||
|
### Conflicts with Current Mainline Truth
|
||||||
|
|
||||||
|
- SQLite/prototype-first database assumptions
|
||||||
|
- hardcoded or stale infrastructure endpoints
|
||||||
|
- alternate Go-first NemoClaw/MCP control center
|
||||||
|
- separate backend root service
|
||||||
|
- stale VPN/headscale-era deployment assumptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Intake Strategy
|
||||||
|
|
||||||
|
### Merge Now Candidates
|
||||||
|
|
||||||
|
- `velocity/api/leads.py`
|
||||||
|
- `velocity/api/kanban.py`
|
||||||
|
- `velocity/api/analytics.py`
|
||||||
|
- `velocity/api/persona.py`
|
||||||
|
- tests supporting the above
|
||||||
|
- selected marketing frontend components
|
||||||
|
- selected iOS camera/time-light feature files
|
||||||
|
|
||||||
|
### Extract, Rewrite, Then Merge
|
||||||
|
|
||||||
|
- `velocity/api/sentinel.py`
|
||||||
|
- `velocity/api/chat_logs.py`
|
||||||
|
- `velocity/services/*`
|
||||||
|
- `frontend/src/lib/api.ts`
|
||||||
|
|
||||||
|
### Do Not Merge Directly
|
||||||
|
|
||||||
|
- `velocity/main.py`
|
||||||
|
- `velocity/main.go`
|
||||||
|
- `velocity/internal/nemoclaw/*`
|
||||||
|
- `velocity/internal/oracle/*`
|
||||||
|
- `velocity/internal/mcp/*`
|
||||||
|
- `velocity/services/comfyui_service.py`
|
||||||
|
|
||||||
|
### Banish Immediately from Merge Candidate Set
|
||||||
|
|
||||||
|
- `.env`
|
||||||
|
- `.pem`
|
||||||
|
- `.db`
|
||||||
|
- `coverage*`
|
||||||
|
- `.coverage`
|
||||||
|
- `.pytest_cache`
|
||||||
|
- `__pycache__`
|
||||||
|
- binaries
|
||||||
|
- logs
|
||||||
|
- scratch outputs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Recommendation
|
||||||
|
|
||||||
|
Sourik’s code should be treated as:
|
||||||
|
|
||||||
|
- **30% importable feature work**
|
||||||
|
- **30% useful reference and test material**
|
||||||
|
- **40% architectural conflict or residue**
|
||||||
|
|
||||||
|
The best path is:
|
||||||
|
|
||||||
|
1. strip all residue first
|
||||||
|
2. import Python feature modules selectively
|
||||||
|
3. import tests aggressively
|
||||||
|
4. compare marketing frontend additions against current app
|
||||||
|
5. compare iOS camera/time-light features against current iOS app
|
||||||
|
6. keep the Go/Nemo/MCP stack out of the first merge unless you deliberately choose to adopt that architecture
|
||||||
|
|
||||||
|
That will give you the value in Sourik’s work without poisoning the current mainline with a second operating model.
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Sourik Root Integration Closure
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This issue tracks the root-side integration of the useful Sourik subsystems into the current Project Velocity mainline without replacing the root FastAPI shell, root Sentinel ownership, or the existing Python-native Oracle v1 surface.
|
||||||
|
|
||||||
|
The objective was to absorb the missing operational pieces into the root codebase so Sprint 1 truth is judged by current product reality rather than by stale planning artifacts.
|
||||||
|
|
||||||
|
## Scope Closed In This Pass
|
||||||
|
|
||||||
|
### CRM backend and sync
|
||||||
|
|
||||||
|
- landed canonical root CRM routes for:
|
||||||
|
- `GET/POST/PUT/DELETE /api/leads`
|
||||||
|
- `GET /api/leads/{lead_id}`
|
||||||
|
- `GET /api/leads/demographics`
|
||||||
|
- `GET/POST /api/chat-logs`
|
||||||
|
- `GET /api/kanban/board`
|
||||||
|
- `PUT /api/kanban/move`
|
||||||
|
- `POST /api/leads/seed-synthetic`
|
||||||
|
- added dedicated CRM websocket stream at `GET /ws/crm`
|
||||||
|
- added root-side CRM event broadcasting for:
|
||||||
|
- lead create
|
||||||
|
- lead update
|
||||||
|
- lead delete
|
||||||
|
- kanban move
|
||||||
|
- chat log create
|
||||||
|
- synthetic seed runs
|
||||||
|
- Oracle writebacks
|
||||||
|
|
||||||
|
### Oracle append and writeback contract
|
||||||
|
|
||||||
|
- kept root Oracle v1 as the primary public Oracle surface
|
||||||
|
- appended persona and workflow orchestration under the existing Oracle v1 flow
|
||||||
|
- added real MCP execution endpoint:
|
||||||
|
- `POST /api/oracle/mcp/execute`
|
||||||
|
- added Oracle action ledger and read/writeback routes:
|
||||||
|
- `GET /api/oracle/actions`
|
||||||
|
- `GET /api/oracle/actions/{action_id}`
|
||||||
|
- `POST /api/oracle/actions/writeback`
|
||||||
|
- Oracle prompt submissions now create persisted action records tied to execution output
|
||||||
|
- Oracle writebacks now support canonical lead updates for:
|
||||||
|
- score adjustments
|
||||||
|
- stage changes
|
||||||
|
- qualification changes
|
||||||
|
- metadata patching
|
||||||
|
- note append
|
||||||
|
- Oracle-generated message insertion into `chat_logs`
|
||||||
|
|
||||||
|
### MCP and search
|
||||||
|
|
||||||
|
- converted the MCP registry from a placeholder slot into an executable root service
|
||||||
|
- external search now executes against:
|
||||||
|
- Brave Search if `BRAVE_API_KEY` is configured
|
||||||
|
- DuckDuckGo fallback otherwise
|
||||||
|
- CRM and local property retrieval tools now execute against the root CRM schema through the root DB pool
|
||||||
|
|
||||||
|
### Catalyst marketing backend parity
|
||||||
|
|
||||||
|
- replaced hardcoded campaign summaries with unified ad-network service backed by root code
|
||||||
|
- added root Meta plus Google Ads unified campaign listing
|
||||||
|
- added unified insights endpoint with platform filtering
|
||||||
|
- added budget update route for Meta plus Google
|
||||||
|
- added bid strategy update route for Meta plus Google
|
||||||
|
- added Google-aware campaign creation path so Catalyst campaign creation is no longer Meta-only
|
||||||
|
|
||||||
|
Routes covered in this pass:
|
||||||
|
|
||||||
|
- `GET /api/catalyst/campaigns`
|
||||||
|
- `POST /api/catalyst/campaigns/create`
|
||||||
|
- `GET /api/catalyst/insights/realtime`
|
||||||
|
- `PUT /api/catalyst/budget`
|
||||||
|
- `PUT /api/catalyst/bid-strategy`
|
||||||
|
|
||||||
|
### Frontend carry-forward
|
||||||
|
|
||||||
|
- preserved the existing root Catalyst shell
|
||||||
|
- kept the vertically stacked `Marketing` sub-tab inside Catalyst
|
||||||
|
- no second marketing app or second frontend API source of truth was introduced
|
||||||
|
|
||||||
|
## Files Added Or Materially Updated
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- `backend/services/ad_network_service.py`
|
||||||
|
- `backend/services/mcp_registry.py`
|
||||||
|
- `backend/api/routes_catalyst.py`
|
||||||
|
- `backend/api/routes_crm.py`
|
||||||
|
- `backend/api/routes_oracle.py`
|
||||||
|
- `backend/oracle/action_service.py`
|
||||||
|
- `backend/oracle/router_v1.py`
|
||||||
|
- `backend/main.py`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `backend/tests/test_catalyst_routes.py`
|
||||||
|
- `backend/tests/test_oracle_routes.py`
|
||||||
|
- `backend/tests/test_crm_websocket.py`
|
||||||
|
|
||||||
|
## Verification Completed
|
||||||
|
|
||||||
|
- `python -m pytest Project_Velocity/backend/tests/test_catalyst_routes.py Project_Velocity/backend/tests/test_oracle_routes.py Project_Velocity/backend/tests/test_crm_websocket.py Project_Velocity/backend/tests/test_crm_routes.py Project_Velocity/backend/tests/oracle/test_persona_service.py Project_Velocity/backend/tests/test_nemoclaw_runtime.py`
|
||||||
|
- result: `9 passed`
|
||||||
|
|
||||||
|
- `npm run build`
|
||||||
|
- result: passed
|
||||||
|
|
||||||
|
## Production Notes
|
||||||
|
|
||||||
|
- Google Ads support is now integrated at the root contract level, but live mutate behavior still depends on valid provider credentials and provider-managed operations.
|
||||||
|
- Brave Search becomes the preferred external search provider when `BRAVE_API_KEY` is present; otherwise the root falls back to DuckDuckGo.
|
||||||
|
- Oracle writebacks currently target leads as the canonical CRM entity. Additional entity writebacks should follow the same `oracle_actions` ledger rather than introducing side-channel writes.
|
||||||
|
|
||||||
|
## Residual Work After This Closure
|
||||||
|
|
||||||
|
These are still separate follow-up items, not blockers for closing this integration pass:
|
||||||
|
|
||||||
|
- deeper Google Ads mutate coverage beyond provider-managed passthroughs
|
||||||
|
- frontend consumption of the CRM websocket stream
|
||||||
|
- broader Oracle writebacks beyond `lead`
|
||||||
|
- stricter auth and role gating for Oracle action application
|
||||||
|
- richer Catalyst campaign creation UX for platform-specific fields
|
||||||
|
- prompt inventory and persona-to-runtime mapping docs cleanup
|
||||||
|
|
||||||
|
## Acceptance Criteria Met
|
||||||
|
|
||||||
|
- root app shell preserved
|
||||||
|
- root FastAPI entrypoint preserved
|
||||||
|
- root Sentinel ownership preserved
|
||||||
|
- no Go runtime adopted
|
||||||
|
- no second backend center introduced
|
||||||
|
- MCP external search executes for real
|
||||||
|
- CRM has a live websocket sync surface
|
||||||
|
- Oracle has a persisted action/writeback contract
|
||||||
|
- Catalyst backend exposes Google-aware parity routes in the root
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
# Project Velocity Pre-Sourik Purge Report
|
||||||
|
|
||||||
|
**Prepared:** 2026-04-12
|
||||||
|
**Scope:** Current `Project_Velocity` codebase excluding `Sourik` for scoring, with a separate compatibility pass against the incoming `Sourik` tree
|
||||||
|
**Purpose:** Identify what should be kept, archived, consolidated, or purged before merging `Sourik` into the main codebase
|
||||||
|
**Action in this report:** No deletion. Assessment only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scoring Model
|
||||||
|
|
||||||
|
| Score | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 | Banish. Strong candidate for removal from repo or immediate quarantine. |
|
||||||
|
| 2 | Very low value. Likely generated, stale, insecure, or misleading. |
|
||||||
|
| 3 | Archive out of main repo. Has historical value but should not live in the main working tree. |
|
||||||
|
| 4 | Review aggressively. Possible duplicate, stale path, or unclear ownership. |
|
||||||
|
| 5 | Transitional. Keep only if a near-term owner confirms it. |
|
||||||
|
| 6 | Useful but not core. Keep if it supports current delivery or migration. |
|
||||||
|
| 7 | Important support artifact. |
|
||||||
|
| 8 | Core implementation path. |
|
||||||
|
| 9 | Highly important operational or product-critical code. |
|
||||||
|
| 10 | Do not purge. Source-of-truth, live runtime, or critical integration surface. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The current codebase has five major purge or merge-hardening problems before `Sourik` is brought in:
|
||||||
|
|
||||||
|
1. **Repo bloat is severe**
|
||||||
|
- `models/` alone is about `13237.61 MB`
|
||||||
|
- `db assets/` is about `3689.78 MB`
|
||||||
|
- `app/` is about `2588.26 MB`
|
||||||
|
- `Payload/` is about `523.81 MB`
|
||||||
|
- `comfy_engine/` is about `524.58 MB`
|
||||||
|
|
||||||
|
2. **The repo contains generated and duplicated artifacts**
|
||||||
|
- `app/dist/` is about `442.56 MB`
|
||||||
|
- `app/public/models/` is about `437.58 MB`
|
||||||
|
- `app/assets/House Floor Plans/` is about `958.41 MB`
|
||||||
|
- the iOS tree has duplicated source files under both:
|
||||||
|
- `iOS/App`, `iOS/Core`, `iOS/Features`
|
||||||
|
- `iOS/velocity/velocity/...`
|
||||||
|
|
||||||
|
3. **There are many zero-byte placeholder files**
|
||||||
|
- especially under `agents/`, `backend/api/`, `backend/database/`, `backend/sentinel/`, `comfy_engine/scripts/`, `infrastructure/aws_scale/`, and `infrastructure/blackbox_local/`
|
||||||
|
- these create false surface area and false merge confidence
|
||||||
|
|
||||||
|
4. **The repo contains security-sensitive or machine-local artifacts**
|
||||||
|
- `desineuron-l4-node.pem` in repo root is unacceptable long-term
|
||||||
|
- historical bootstrap scripts contain stale IPs, stale assumptions, and one-off deployment logic
|
||||||
|
|
||||||
|
5. **The incoming Sourik tree overlaps the most dangerous places**
|
||||||
|
- backend API surface
|
||||||
|
- frontend API client and dashboard/marketing modules
|
||||||
|
- Oracle/Catalyst/Sentinel concepts
|
||||||
|
- alternative runtime stacks in Go/Python
|
||||||
|
|
||||||
|
The highest-priority pre-merge action is not deleting business logic. It is removing ambiguity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quantitative Findings
|
||||||
|
|
||||||
|
### Largest top-level paths
|
||||||
|
|
||||||
|
| Path | Size |
|
||||||
|
| --- | ---: |
|
||||||
|
| `models/` | `13237.61 MB` |
|
||||||
|
| `db assets/` | `3689.78 MB` |
|
||||||
|
| `app/` | `2588.26 MB` |
|
||||||
|
| `comfy_engine/` | `524.58 MB` |
|
||||||
|
| `Payload/` | `523.81 MB` |
|
||||||
|
| `.Agent Context/` | `8.92 MB` |
|
||||||
|
| `backend/` | `0.40 MB` |
|
||||||
|
| `iOS/` | `0.33 MB` |
|
||||||
|
| `infrastructure/` | `0.27 MB` |
|
||||||
|
|
||||||
|
### Generated or duplicated asset-heavy paths
|
||||||
|
|
||||||
|
| Path | Size | Assessment |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `app/dist/` | `442.56 MB` | Generated build output. Should not live in source control as working source. |
|
||||||
|
| `app/public/models/` | `437.58 MB` | Runtime-facing duplicates of model assets. |
|
||||||
|
| `app/assets/House Floor Plans/` | `958.41 MB` | Source asset tree with duplicate downstream copies. |
|
||||||
|
| `db assets/` | `3689.78 MB` | Valuable business/demo data, but too heavy for a clean app repo. |
|
||||||
|
| `models/` | `13237.61 MB` | Runtime model binaries. Belongs in artifact storage, not Git. |
|
||||||
|
| `Payload/` | `523.81 MB` | Packaged payload/archive staging. Likely not source-of-truth. |
|
||||||
|
|
||||||
|
### Zero-byte files found
|
||||||
|
|
||||||
|
Representative zero-byte paths include:
|
||||||
|
|
||||||
|
- `agents/docker-compose.agents.yml`
|
||||||
|
- `agents/openclaw_gateway/openclaw.json`
|
||||||
|
- `agents/openclaw_gateway/workspace/AGENTS.md`
|
||||||
|
- `agents/openclaw_gateway/workspace/HEARTBEAT.md`
|
||||||
|
- `agents/skills/meta_ads_manager.py`
|
||||||
|
- `agents/skills/social_publisher.py`
|
||||||
|
- `agents/skills/whatsapp_connector.ts`
|
||||||
|
- `backend/api/routes_crm.py`
|
||||||
|
- `backend/api/routes_oracle.py`
|
||||||
|
- `backend/api/routes_weaver.py`
|
||||||
|
- `backend/database/pinecone_client.py`
|
||||||
|
- `backend/database/schemas.py`
|
||||||
|
- `backend/database/supabase_client.py`
|
||||||
|
- `backend/sentinel/face_tracker.py`
|
||||||
|
- `backend/sentinel/sentiment_engine.py`
|
||||||
|
- `comfy_engine/scripts/auto_term_sheet.py`
|
||||||
|
- `comfy_engine/scripts/queue_manager.py`
|
||||||
|
- `comfy_engine/workflows/cinematic_wan22_14b.json`
|
||||||
|
- `comfy_engine/workflows/dream_weaver_restyle.json`
|
||||||
|
- `infrastructure/aws_scale/node1_agents.tf`
|
||||||
|
- `infrastructure/aws_scale/node2_rendering.tf`
|
||||||
|
- `infrastructure/aws_scale/tailscale_config.sh`
|
||||||
|
- `infrastructure/blackbox_local/docker-compose.local.yml`
|
||||||
|
- `infrastructure/blackbox_local/setup_gpu_env.sh`
|
||||||
|
|
||||||
|
These files are more dangerous than absent files because they imply functionality that does not exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Duplicate Code and Structural Redundancy
|
||||||
|
|
||||||
|
### iOS duplicate source trees
|
||||||
|
|
||||||
|
The following file sets are byte-identical duplicates:
|
||||||
|
|
||||||
|
- `iOS/App/ContentView.swift`
|
||||||
|
- `iOS/velocity/velocity/App/ContentView.swift`
|
||||||
|
|
||||||
|
- `iOS/App/VelocityApp.swift`
|
||||||
|
- `iOS/velocity/velocity/App/VelocityApp.swift`
|
||||||
|
|
||||||
|
- `iOS/Core/State/AppStore.swift`
|
||||||
|
- `iOS/velocity/velocity/Core/State/AppStore.swift`
|
||||||
|
|
||||||
|
- `iOS/Core/UI/GlassBlurView.swift`
|
||||||
|
- `iOS/velocity/velocity/Core/UI/GlassBlurView.swift`
|
||||||
|
|
||||||
|
- `iOS/Core/UI/VelocityTheme.swift`
|
||||||
|
- `iOS/velocity/velocity/Core/UI/VelocityTheme.swift`
|
||||||
|
|
||||||
|
- `iOS/Features/Dashboard/DashboardView.swift`
|
||||||
|
- `iOS/velocity/velocity/Features/Dashboard/DashboardView.swift`
|
||||||
|
|
||||||
|
- `iOS/Features/Oracle/OracleView.swift`
|
||||||
|
- `iOS/velocity/velocity/Features/Oracle/OracleView.swift`
|
||||||
|
|
||||||
|
- `iOS/Features/Sentinel/SentinelView.swift`
|
||||||
|
- `iOS/velocity/velocity/Features/Sentinel/SentinelView.swift`
|
||||||
|
|
||||||
|
- `iOS/Features/Settings/SettingsView.swift`
|
||||||
|
- `iOS/velocity/velocity/Features/Settings/SettingsView.swift`
|
||||||
|
|
||||||
|
Interpretation:
|
||||||
|
|
||||||
|
- one tree is acting like a clean source mirror
|
||||||
|
- the other is the real Xcode project path
|
||||||
|
- keeping both guarantees confusion and future merge drift
|
||||||
|
|
||||||
|
### Frontend demo/runtime split
|
||||||
|
|
||||||
|
There is a real split between:
|
||||||
|
|
||||||
|
- polished app shell modules in `app/src/components/modules/*`
|
||||||
|
- newer Oracle-specific implementation under `app/src/oracle/*`
|
||||||
|
- demo fallback content in `app/src/oracle/lib/oracleDemoData.ts`
|
||||||
|
|
||||||
|
This is not purge-worthy by itself, but it is a clear merge-risk zone because multiple “truths” exist:
|
||||||
|
|
||||||
|
- product shell truth
|
||||||
|
- demo truth
|
||||||
|
- intended production truth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scored Purge Table
|
||||||
|
|
||||||
|
| Path / Scope | Type | Score | Recommendation | Reason |
|
||||||
|
| --- | --- | ---: | --- | --- |
|
||||||
|
| `desineuron-l4-node.pem` | file | 1 | Banish from repo immediately | Private key material does not belong in source control. This is a security risk, not just clutter. |
|
||||||
|
| `3.0.0` | file | 1 | Banish | Zero-value stray file with no semantic role. |
|
||||||
|
| `app/dist/` | dir | 1 | Banish from repo, generate in CI/local only | Build output. Large, derived, and guaranteed merge noise. |
|
||||||
|
| `agents/docker-compose.agents.yml` | file | 1 | Banish unless populated immediately | Zero-byte placeholder that implies a deployable agents stack that does not exist. |
|
||||||
|
| `agents/openclaw_gateway/openclaw.json` | file | 1 | Banish or implement | Zero-byte config placeholder. |
|
||||||
|
| `agents/openclaw_gateway/workspace/AGENTS.md` | file | 1 | Banish | Zero-byte placeholder. |
|
||||||
|
| `agents/openclaw_gateway/workspace/HEARTBEAT.md` | file | 1 | Banish | Zero-byte placeholder. |
|
||||||
|
| `agents/skills/meta_ads_manager.py` | file | 1 | Banish or implement elsewhere | Empty skill file creates false feature surface. |
|
||||||
|
| `agents/skills/social_publisher.py` | file | 1 | Banish or implement elsewhere | Same issue. |
|
||||||
|
| `agents/skills/whatsapp_connector.ts` | file | 1 | Banish or implement elsewhere | Same issue. |
|
||||||
|
| `backend/api/routes_crm.py` | file | 1 | Banish or fill before merge | Empty route file conflicts conceptually with live backend ownership. |
|
||||||
|
| `backend/api/routes_oracle.py` | file | 1 | Banish or redirect to real Oracle router | Empty file directly conflicts with actual Oracle implementation path in `backend/oracle/router_v1.py`. |
|
||||||
|
| `backend/api/routes_weaver.py` | file | 1 | Banish or implement | Empty route file. |
|
||||||
|
| `backend/database/pinecone_client.py` | file | 1 | Banish | Empty placeholder, no runtime value. |
|
||||||
|
| `backend/database/schemas.py` | file | 1 | Banish | Empty placeholder; current schema truth lives elsewhere. |
|
||||||
|
| `backend/database/supabase_client.py` | file | 1 | Banish | Empty placeholder and architecturally confusing because current truth favors PostgreSQL-first. |
|
||||||
|
| `backend/sentinel/face_tracker.py` | file | 1 | Banish | Empty placeholder. |
|
||||||
|
| `backend/sentinel/sentiment_engine.py` | file | 1 | Banish | Empty placeholder. |
|
||||||
|
| `comfy_engine/scripts/auto_term_sheet.py` | file | 1 | Banish | Empty placeholder. |
|
||||||
|
| `comfy_engine/scripts/queue_manager.py` | file | 1 | Banish | Empty placeholder. |
|
||||||
|
| `comfy_engine/workflows/cinematic_wan22_14b.json` | file | 1 | Banish or replace with real workflow export | Empty workflow file is actively misleading. |
|
||||||
|
| `comfy_engine/workflows/dream_weaver_restyle.json` | file | 1 | Banish or replace with real workflow export | Same issue. |
|
||||||
|
| `infrastructure/aws_scale/node1_agents.tf` | file | 1 | Banish or implement in a real infra module | Empty Terraform file is dead weight. |
|
||||||
|
| `infrastructure/aws_scale/node2_rendering.tf` | file | 1 | Banish or implement in a real infra module | Same issue. |
|
||||||
|
| `infrastructure/aws_scale/tailscale_config.sh` | file | 1 | Banish | Empty and also contradicts current non-Tailscale operating direction. |
|
||||||
|
| `infrastructure/blackbox_local/docker-compose.local.yml` | file | 1 | Banish or define properly | Empty infra stub. |
|
||||||
|
| `infrastructure/blackbox_local/setup_gpu_env.sh` | file | 1 | Banish or define properly | Empty infra stub. |
|
||||||
|
| `backend_deploy_20260401.tgz` | file | 2 | Archive outside repo | Historical deploy artifact, not source. |
|
||||||
|
| `Payload/comfy_engine.zip` | file | 2 | Archive outside repo | Packaged artifact. Valuable for history, not for source control. |
|
||||||
|
| `remote_bootstrap_20260401.sh` | file | 3 | Archive under runbooks or infra-history | Historical one-off bootstrap with time-bound assumptions. |
|
||||||
|
| `patch_nemoclaw_service_20260401.sh` | file | 3 | Archive | One-off patch script, not durable runtime code. |
|
||||||
|
| `user_data_bootstrap.sh` | file | 4 | Keep only if still current; otherwise archive to infra-history | Contains useful S3/bootstrap logic, but also bakes old assumptions. |
|
||||||
|
| `dw_gateway_v2_min.py` | file | 4 | Review for consolidation into backend or comfy service layer | Functional, but sits as a root-level one-off service, not in an owned module. |
|
||||||
|
| `monitor_nvme.py` | file | 4 | Archive to ops scripts if still useful | Ad hoc operational helper, not core product code. |
|
||||||
|
| `monitor_qwen.py` | file | 4 | Archive to ops scripts if still useful | Same issue. |
|
||||||
|
| `test_scp.txt` | file | 1 | Banish | Stray scratch file. |
|
||||||
|
| `app/public/models/` | dir | 3 | Review for consolidation | Likely runtime-facing duplicates of source model assets. |
|
||||||
|
| `app/assets/House Floor Plans/` | dir | 5 | Keep as design/source assets, but deduplicate outputs | Real source value exists, but there are duplicate exports in `public/` and `dist/`. |
|
||||||
|
| `db assets/` | dir | 5 | Move to data repository or artifact store | Valuable for demo and product data, but too heavy and merge-hostile for application repo. |
|
||||||
|
| `models/` | dir | 2 | Move to S3/artifact store, keep manifest only in repo | Runtime models should not live in Git. Canonical storage should be S3 or controlled local store. |
|
||||||
|
| `iOS/App`, `iOS/Core`, `iOS/Features` | dir tree | 4 | Consolidate with actual Xcode tree | Duplicates the active app tree and guarantees drift. |
|
||||||
|
| `iOS/velocity/velocity/...` | dir tree | 8 | Keep as likely real app source-of-truth | This appears to be the actual project-backed path tied to Xcode. |
|
||||||
|
| `app/src/oracle/lib/oracleDemoData.ts` | file | 5 | Keep short-term, mark explicitly as demo-only | Useful for UI continuity, but dangerous if mistaken for live data path. |
|
||||||
|
| `app/src/lib/oracleQueryClient.ts` | file | 6 | Review against `app/src/oracle/lib/oracleApiClient.ts` | Potential overlap between legacy Oracle client and new Oracle path. |
|
||||||
|
| `app/src/oracle/lib/oracleApiClient.ts` | file | 8 | Keep | Newer Oracle contract path. |
|
||||||
|
| `backend/main.py` | file | 10 | Keep | Current backend integration root. |
|
||||||
|
| `backend/routers/sentinel.py` | file | 10 | Keep | Live Sentinel backbone. |
|
||||||
|
| `backend/routers/cctv.py` | file | 9 | Keep | Active auto-mode/CCTV path. |
|
||||||
|
| `backend/routers/videos.py` | file | 9 | Keep | Active video catalog runtime path. |
|
||||||
|
| `backend/routers/vault.py` | file | 9 | Keep | Live vault and trackable-link logic. |
|
||||||
|
| `backend/services/nemoclaw_client.py` | file | 10 | Keep | Current AI runtime integration truth. |
|
||||||
|
| `backend/services/auto_mode_matcher.py` | file | 9 | Keep | Core Sentinel automation logic. |
|
||||||
|
| `backend/oracle/router_v1.py` | file | 9 | Keep | Real Oracle implementation path, even if not fully mounted in the main runtime. |
|
||||||
|
| `backend/oracle/*` | dir tree | 8 | Keep | Important product direction with tests and contracts. |
|
||||||
|
| `backend/tests/oracle/*` | dir tree | 8 | Keep | High-value protection for a fragile future merge area. |
|
||||||
|
| `app/src/components/modules/Sentinel.tsx` and `sentinel/*` | module area | 9 | Keep | Core UI for a real implemented subsystem. |
|
||||||
|
| `app/src/components/modules/Catalyst.tsx` | file | 7 | Keep, but compare against Sourik marketing/catalyst ideas | Active surface with likely future overlap. |
|
||||||
|
| `app/src/components/modules/Oracle.tsx` + `app/src/app/oracle/page.tsx` | module area | 9 | Keep | Core UI architecture for Oracle. |
|
||||||
|
| `infrastructure/desineuron_ingress/*` | dir tree | 10 | Keep | Live infrastructure truth and runbooks. |
|
||||||
|
| `infrastructure/ops_control_plane/*` | dir tree | 10 | Keep | Live operational control surface. |
|
||||||
|
| `.Agent Context/Bibels/*` | docs | 8 | Keep and consolidate | High-value product/infra truth. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Strong Retain Set
|
||||||
|
|
||||||
|
These are the areas that should be treated as current core code, not purge targets:
|
||||||
|
|
||||||
|
- `backend/main.py`
|
||||||
|
- `backend/routers/*` except empty placeholder files in `backend/api/`
|
||||||
|
- `backend/services/*`
|
||||||
|
- `backend/oracle/*`
|
||||||
|
- `backend/db/*`
|
||||||
|
- `backend/tests/*`
|
||||||
|
- `app/src/*` excluding clearly demo-only or overlapping legacy Oracle client paths that need review
|
||||||
|
- `infrastructure/desineuron_ingress/*`
|
||||||
|
- `infrastructure/ops_control_plane/*`
|
||||||
|
- `iOS/velocity/velocity/*` as the likely canonical iOS app path
|
||||||
|
- `.Agent Context/Bibels/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redundant or Misleading Areas
|
||||||
|
|
||||||
|
### 1. The agents subtree
|
||||||
|
|
||||||
|
Current assessment:
|
||||||
|
|
||||||
|
- almost entirely placeholder surface
|
||||||
|
- no runtime truth
|
||||||
|
- no concrete ownership
|
||||||
|
- high probability of direct collision with Sourik’s actual agents and MCP work
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
|
||||||
|
- do **not** merge any existing non-Sourik `agents/` placeholders forward as if they are real
|
||||||
|
- either delete them later or move them to a scratch/archive area
|
||||||
|
|
||||||
|
### 2. Duplicate iOS trees
|
||||||
|
|
||||||
|
Current assessment:
|
||||||
|
|
||||||
|
- exact duplicate code across two trees
|
||||||
|
- one likely mirrors source
|
||||||
|
- the nested Xcode-backed tree is the practical source of truth
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
|
||||||
|
- pick one tree before large merges
|
||||||
|
- otherwise every iOS merge becomes a double-merge
|
||||||
|
|
||||||
|
### 3. Root-level ops scripts
|
||||||
|
|
||||||
|
Current assessment:
|
||||||
|
|
||||||
|
- some are useful operational history
|
||||||
|
- none should remain ambiguous root-level “maybe runtime” assets
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
|
||||||
|
- either move to `infrastructure/archive/` or formalize them under owned infra modules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility Assessment for Incoming Sourik Merge
|
||||||
|
|
||||||
|
This section is based on the **shape** of the incoming `Sourik` tree and how it overlaps with the current codebase.
|
||||||
|
|
||||||
|
### High-Conflict Zones
|
||||||
|
|
||||||
|
| Incoming Sourik Area | Current Main Area | Risk | Why |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Sourik/velocity/api/*.py` | `backend/main.py`, `backend/routers/*`, `backend/api/*` | High | Both define backend API surfaces for Catalyst, Sentinel, leads, websockets, and chat/log flows. |
|
||||||
|
| `Sourik/velocity/frontend/src/lib/api.ts` | `app/src/lib/api.ts`, `app/src/oracle/lib/oracleApiClient.ts` | High | Conflicting client-side contract layers are likely. |
|
||||||
|
| `Sourik/velocity/frontend/src/components/...` | `app/src/components/modules/*` | High | Dashboard, marketing, and visual modules are likely to overlap semantically. |
|
||||||
|
| `Sourik/velocity/internal/oracle/*` | `backend/oracle/*` | High | Two different Oracle implementations or mental models may coexist. |
|
||||||
|
| `Sourik/velocity/internal/nemoclaw/*` | `backend/services/nemoclaw_client.py`, prompt files | High | Strong conceptual overlap around reasoning/runtime responsibilities. |
|
||||||
|
| `Sourik/velocity/services/comfyui_service.py` | `infrastructure/desineuron_ingress/*`, ops control plane, current Comfy deployment path | High | Comfy runtime ownership is already established elsewhere. |
|
||||||
|
| `Sourik/VelocityApp/*` | `iOS/velocity/velocity/*` | Medium-High | iOS naming and feature overlap likely. |
|
||||||
|
|
||||||
|
### Medium-Conflict Zones
|
||||||
|
|
||||||
|
| Incoming Sourik Area | Current Main Area | Risk | Why |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Sourik/velocity/marketing/*` | `backend/api/routes_catalyst.py`, `app/src/components/modules/Catalyst.tsx` | Medium | Similar domain, different implementation lineage. |
|
||||||
|
| `Sourik/velocity/mcp/*` | current repo has almost no real MCP implementation outside ops/docs | Medium | Less direct code collision, but conceptual ownership must be assigned. |
|
||||||
|
| `Sourik/velocity/integrations/*` | current backend service boundaries | Medium | Could be useful if treated as adapter candidates instead of merged blindly. |
|
||||||
|
|
||||||
|
### Low-Compatibility / High-Archive Areas in Sourik
|
||||||
|
|
||||||
|
These should not be merged into main as-is later:
|
||||||
|
|
||||||
|
- `.pytest_cache`
|
||||||
|
- compiled `.pyc`
|
||||||
|
- `.env`
|
||||||
|
- local `prototype.db`
|
||||||
|
- `coverage*`
|
||||||
|
- test output logs
|
||||||
|
- bundled `.exe`
|
||||||
|
- scratch guides and extracted intermediates
|
||||||
|
- duplicate keys such as `desineuron-l4-node.pem`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Merge Filter Recommendations
|
||||||
|
|
||||||
|
Before merging `Sourik`, do this sequence:
|
||||||
|
|
||||||
|
1. **Purge or quarantine score 1-2 paths from the current main tree**
|
||||||
|
- especially secrets, zero-byte placeholders, build output, and archived binaries
|
||||||
|
|
||||||
|
2. **Resolve canonical owners**
|
||||||
|
- iOS canonical tree
|
||||||
|
- Oracle backend owner path
|
||||||
|
- Comfy/infra owner path
|
||||||
|
- AI runtime owner path
|
||||||
|
|
||||||
|
3. **Mark demo-only code explicitly**
|
||||||
|
- especially Oracle demo fallback and any mock client layers
|
||||||
|
|
||||||
|
4. **Move heavy artifacts out of repo**
|
||||||
|
- models
|
||||||
|
- generated builds
|
||||||
|
- payload zips
|
||||||
|
- bulky demo assets if they are not required in the core app repo
|
||||||
|
|
||||||
|
5. **Merge Sourik by subsystem, not by tree**
|
||||||
|
- backend API
|
||||||
|
- frontend marketing/dashboard
|
||||||
|
- Oracle logic
|
||||||
|
- MCP/integrations
|
||||||
|
- iOS
|
||||||
|
|
||||||
|
6. **Do not allow two runtimes for the same responsibility**
|
||||||
|
- one Oracle backend
|
||||||
|
- one Comfy orchestration path
|
||||||
|
- one Nemoclaw integration contract
|
||||||
|
- one frontend API client contract per domain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Recommendation
|
||||||
|
|
||||||
|
The current codebase is not yet ready for a blind merge with `Sourik`.
|
||||||
|
|
||||||
|
The biggest blockers are:
|
||||||
|
|
||||||
|
- fake surface area from empty files
|
||||||
|
- duplicated iOS source roots
|
||||||
|
- heavy runtime assets in Git
|
||||||
|
- root-level operational drift
|
||||||
|
- overlapping conceptual ownership around Oracle, Nemoclaw, Catalyst, and API contracts
|
||||||
|
|
||||||
|
The right move is:
|
||||||
|
|
||||||
|
- purge ambiguity first
|
||||||
|
- then merge subsystem by subsystem
|
||||||
|
|
||||||
|
That will save far more time than trying to reconcile two partially overlapping stacks after the fact.
|
||||||
|
|
||||||
338
.Agent Context/Sprint 1/Sprint 1 Fact Table - 2026-04-12.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# Sprint 1 Fact Table
|
||||||
|
|
||||||
|
**Date:** 2026-04-12
|
||||||
|
**Scope:** Reconciliation of `userstories.csv` and `tasks.csv` against the current Project Velocity repo, live infrastructure, and evolved product direction
|
||||||
|
**Purpose:** Show what is done, partial, changed, deferred to v2, or still missing
|
||||||
|
|
||||||
|
**Important Note:** `tasks.csv` and `userstories.csv` contain many items marked `Closed`. That is treated here as historical planning metadata, not source-of-truth delivery status. This fact table is based on the current repo, live infrastructure, and the product direction clarified after Sprint 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status Key
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Done` | Implemented in current mainline or achieved through an evolved equivalent |
|
||||||
|
| `Partial` | Significant work exists, but the original intent is not fully complete |
|
||||||
|
| `Changed` | Original task was superseded or split into a better/different architecture |
|
||||||
|
| `V2` | Intentionally better treated as v2 or later, not a Sprint 1 blocker now |
|
||||||
|
| `Missing` | Not materially implemented in mainline yet |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
### User Story Rollup
|
||||||
|
|
||||||
|
| User Story | Status | Reality |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1.1 Local/Cloud Hardware Config` | `Changed` | The current AWS/Linux stack is simulation hardware for product proving. The client-facing recommendation is now minimum `1x NVIDIA RTX PRO 6000 Blackwell 96GB`, recommended `2x`, with horizontal scaling and DGX/H100-class builds for HNI clients. |
|
||||||
|
| `1.2 ComfyUI Visual Workflows` | `Partial` | Dream Weaver, Qwen poster, and Wan 2.2 workflow assets all exist locally as real code and workflow JSONs. The remaining gap is production-grade orchestration, async automation, and canonical packaging. |
|
||||||
|
| `1.3 System Prompts & UI Logic` | `Partial` | Prompt assets and UI direction exist, but there is no single canonical prompt inventory mapping persona -> function -> file -> model -> API surface. |
|
||||||
|
| `2.1 Swift/iPad App` | `Partial` | The iOS app is materially implemented and was proven once in simulated form. The blocker is stable endpoint contract and environment passthrough, not absence of implementation. |
|
||||||
|
| `2.2 FastAPI Neural Core` | `Done` | FastAPI backend, PostgreSQL, auth, Sentinel stack, vault, scenes, videos, and live infra are in place. This is the real operating backend, not a placeholder. |
|
||||||
|
| `2.3 CRM/WebOS React Wiring` | `Partial` | WebOS shell is strong, but the canonical CRM backend contract is incomplete. Leads/chat/kanban wiring is still not fully landed in mainline. |
|
||||||
|
| `3.1 Claw Bot Ecosystem` | `Changed` | The original OpenClaw framing is obsolete. The intended truth is NemoClaw as the operating agent inside on-prem or client-cloud deployments, with stronger local-model autonomy planned later. |
|
||||||
|
| `3.2 MCP Server/Tools` | `V2` | MCP work is deliberate in Sourik's tree, but it is not part of the current mainline operating center yet. |
|
||||||
|
| `3.3 Marketing Automation` | `Partial` | Meta/Catalyst foundations exist and visual generation is wired conceptually, but Google Ads, autonomous posting, and a unified production-grade campaign loop remain open. |
|
||||||
|
| `4.1 Future Life and Time & Light Engine` | `Changed` | This story has effectively split: `Future Life` is now v2, while `Time & Light` remains a v1 item with significant implementation already present in iOS. |
|
||||||
|
| `4.2 Engagement Intelligence and Social Proof layer` | `Partial` | Sentinel intelligence exists, and there is a real browser-webcam perception path using Google MediaPipe on the frontend. The sales-facing social proof, room-peak, and wealth/legacy payoff layers are not fully delivered yet. |
|
||||||
|
|
||||||
|
### Summary Counts
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
| --- | ---: |
|
||||||
|
| `Done` | 1 |
|
||||||
|
| `Partial` | 6 |
|
||||||
|
| `Changed` | 3 |
|
||||||
|
| `V2` | 1 |
|
||||||
|
| `Missing` | 0 |
|
||||||
|
|
||||||
|
Interpretation:
|
||||||
|
|
||||||
|
- Sprint 1 was not a failure
|
||||||
|
- the repo contains more real implementation than the old fact table gave credit for
|
||||||
|
- the biggest drift is not infra anymore
|
||||||
|
- the biggest drift is CRM completion, stable app/backend contracts, prompt inventory, and productized execution paths
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Story Fact Table
|
||||||
|
|
||||||
|
| User Story | Original Intent | Current Status | Evidence / Current Truth | What Remains |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `1.1 Local/Cloud Hardware Config` | Define Black Box + provision 8xA100 + split nodes + tunnels | `Changed` | Current infra is a proving ground: Linux control surface, AWS GPU workers, S3 strategy, stable `t4g.micro` ingress. For clients, the target recommendation is minimum `1x RTX PRO 6000 Blackwell 96GB`, recommended `2x`, and horizontal scaling if workload grows. HNI builders can justify DGX/H100-class systems. | Formalize the customer hardware matrix, scaling tiers, and on-prem packaging playbook. |
|
||||||
|
| `1.2 ComfyUI Visual Workflows` | Dream Weaver, poster gen, Wan 2.2, async queue API | `Partial` | The repo contains real workflow assets and code: `comfy_engine/workflows/dreamweaver_phase1_depth.json`, `dreamweaver_phase2_multicontrol.json`, `dreamweaver_phase3_batch.json`, `dreamweaver_a100_human_preservation.json`, `catalyst_poster_qwen.json`, `cinematic_wan22_14b.json`, plus `dw_gateway_v2.py`, `queue_manager.py`, and batch/test scripts. | Standardize canonical workflow packaging, unify gateway/runtime assumptions, and finish end-to-end async automation across all workflows. |
|
||||||
|
| `1.3 System Prompts & UI Logic` | Oracle persona prompts, Catalyst prompts, lock frontend/API design | `Partial` | Oracle architecture is deeply specified, and prompt assets already exist in `backend/nemoclaw_prompts/*.md` plus Dream Weaver prompt files. UI language exists. | Build a first-principles prompt inventory: which personas/functions exist, which prompt files are canonical, where Oracle prompts live, where Catalyst prompts live, and what API contracts they expect. |
|
||||||
|
| `2.1 Swift/iPad App` | Native shell + camera + sun path | `Partial` | The app is materially implemented: `ComfyClient.swift`, `InventoryView.swift`, `ARSunOverlayView.swift`, `SunMath.swift`, `SimulatorSunOverlayView.swift`, and slider/camera flow are present in the canonical iOS tree. It has been demonstrated once in simulated form from Sayan's MacBook. | Stabilize environment passthrough and endpoint mapping so the existing app can reliably talk to the current backend/Comfy gateways after code merge. |
|
||||||
|
| `2.2 FastAPI Neural Core` | FastAPI + PostgreSQL + endpoints + websockets | `Done` | `backend/main.py` is a real unified backend with DB pool lifecycle, auth, Catalyst, Sentinel, CCTV, scenes, videos, vault, static assets, and websocket support. This is already a live operating backend rather than a stub. | Expand feature breadth, not existence: CRM routes, Oracle mounting/contract cleanup, and stricter API/schema discipline. |
|
||||||
|
| `2.3 CRM/WebOS React Wiring` | Connect WebOS to backend, Kanban, dashboard sentiment | `Partial` | The WebOS/UI layer is real, but the backend contract is incomplete. `backend/api/routes_crm.py` is zero bytes, `backend/api/routes_oracle.py` is zero bytes, and the frontend still uses `app/src/components/oracle/mockLeads.ts`. `PipelineView.tsx` and CRM types exist, but they are not backed by canonical live endpoints yet. | Finish the real lead/chat/kanban/oracle endpoints, replace mock lead data, wire CRM websocket updates, define DB structure, and generate synthetic client data for verification. |
|
||||||
|
| `3.1 Claw Bot Ecosystem` | Deploy OpenClaw as primary communication agent | `Changed` | The product direction is now NemoClaw as the operating agent for client on-prem or client-cloud deployments. Current reality still uses the Python backend as the main runtime center, with future agent hardening planned. | Define the v1 NemoClaw boundary cleanly: what it owns now, how it integrates with CRM and Catalyst, and what shifts to local `30B/70B` models in v2. |
|
||||||
|
| `3.2 MCP Server/Tools` | Local files, DB, internet via MCP | `V2` | MCP work exists deliberately in Sourik's code and should be treated as an explicit future subsystem, not accidental residue. | Revisit only after Sayan approval and after the core CRM/product surfaces stabilize enough to absorb it cleanly. |
|
||||||
|
| `3.3 Marketing Automation` | Meta/Google, bids, content generation, posting | `Partial` | Mainline has a substantial Catalyst UI in `app/src/components/modules/Catalyst.tsx` and live Meta-oriented backend routes in `backend/api/routes_catalyst.py` for campaign creation, creative sync, insights, lookalike audiences, and auth. Qwen/Wan workflow references are already reflected in the UI. | Missing pieces are Google Ads parity, autonomous posting, unified audience/budget/bid loop, production-grade marketing DB/state, and a non-fragmented front-to-back execution path. |
|
||||||
|
| `4.1 Future Life and Time & Light Engine` | Future Life videos + iPad time/light engine | `Changed` | The story is now two scopes. `Future Life` is pushed to v2. `Time & Light` is materially implemented in iOS via AR sun overlay, sun math, SceneKit dollhouse, sliders, and simulator fallback. | For v1, finish endpoint stability, validation on real devices, and tighten the product surface around Time & Light. For v2, Future Life needs a defined cinematic workflow and asset/runtime contract. |
|
||||||
|
| `4.2 Engagement Intelligence and Social Proof layer` | Social proof, emotional anchoring, wealth projection | `Partial` | Sentinel/QD logic exists, and the repo already contains a live-session browser path for webcam perception: `PerceptionPlayer.tsx` captures webcam, `useMediapipeFaceLandmarker.ts` runs Google MediaPipe in-browser, `landmarkPacketEncoder.ts` emits compact blendshape packets, and `backend/routers/sentinel.py` ingests them over WebSocket and returns QD updates. Your intended broader testing path can also use browser automation and public/free CCTV feeds. | Validate the live MacBook browser path end-to-end with Sayan, then decide how much additional CCTV/browser testing is needed before productizing the sales-facing social proof and room-peak layers. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Fact Table
|
||||||
|
|
||||||
|
| Ref | Task | Status | Fact | Notes |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `W1-2` | Define local Black Box edge server requirements | `Partial` | The architectural direction is now clear, but the customer-facing hardware matrix is still not formalized in one deployment standard. | Document minimum `1x RTX PRO 6000 Blackwell 96GB`, recommended `2x`, horizontal scaling, and DGX/H100 escalation path. |
|
||||||
|
| `W2-3` | Provision AWS 8xA100 instance | `Changed` | Current AWS GPU use is validation hardware, not the final customer recommendation. | The live L4/G6 path served testing; product packaging should now reference Blackwell-class on-prem targets. |
|
||||||
|
| `W2-4` | Virtualize AWS into Node 1 and Node 2 | `Changed` | Not how the system evolved. | Replaced by Linux control plane + ephemeral AWS workers. |
|
||||||
|
| `W2-5` | Secure SSH tunnel access | `Changed` | Stable ingress, SSM, LAN access, and route sync superseded this. | Current system is better than the original tunnel-only idea. |
|
||||||
|
| `W1-7` | Dream Weaver interior restyling workflow | `Partial` | Real workflow JSONs, gateway code, prompt expansion, mask preprocessing, A100 executor, and batch processor exist locally. | Convert the local workflow stack into a canonical production package with stable endpoints and validation. |
|
||||||
|
| `W1-8` | Marketing poster workflow using Qwen-Image 2512 | `Partial` | `comfy_engine/workflows/catalyst_poster_qwen.json` exists and the Catalyst surface references Qwen poster generation directly. | Make the poster flow an operator-safe product path instead of a local workflow asset plus operational runs. |
|
||||||
|
| `W2-9` | Wan 2.2 video generation workflow | `Partial` | `comfy_engine/workflows/cinematic_wan22_14b.json` exists and the Catalyst UI explicitly models Wan 2.2 generation states. | Productionize and validate the actual operator/runtime path; keep broader Future Life cinematic orchestration in v2. |
|
||||||
|
| `W2-10` | Expose all ComfyUI workflows via Async Queue API | `Partial` | `dw_gateway_v2.py`, `dw_gateway_v2_min.py`, `queue_manager.py`, and iOS `ComfyClient.swift` show a real queue/poll/result model exists. | Unify this into one canonical async API across Dream Weaver, Qwen poster, and Wan video workflows. |
|
||||||
|
| `W1-12` | Draft Oracle persona prompts | `Partial` | Prompt assets exist across current and parallel work. | Final product-grade persona contract still needs consolidation. |
|
||||||
|
| `W1-13` | Create Catalyst marketing prompts | `Partial` | Partial logic exists in Catalyst and service layers. | Build a canonical prompt map and separate strategy prompts from execution prompts. |
|
||||||
|
| `W1-14` | Lock frontend UI design and API schemas | `Partial` | Premium UI direction exists strongly. | API schemas are still evolving, especially for Oracle and CRM. |
|
||||||
|
| `W1-16` | Build native SwiftUI app shell | `Done` | Mainline iOS shell and modules exist. | Feature stabilization is separate. |
|
||||||
|
| `W1-17` | Camera capture feature to ComfyUI | `Partial` | Mainline iOS includes `CameraPicker` and a real `ComfyClient` talking to a Dream Weaver gateway. | Stabilize live endpoints and prove the flow again after code consolidation. |
|
||||||
|
| `W1-18` | Sun Path overlay in iPad app | `Partial` | ARKit, CoreLocation, CoreMotion, SceneKit, `SunMath`, and simulator fallback are implemented in the canonical iOS tree. | Real-device acceptance, UX tightening, and full passthrough validation remain. |
|
||||||
|
| `W1-20` | Marketing page frontend for Sourik | `Partial` | Mainline has a substantial Catalyst module already, even though Sourik also built a separate marketing page. | Decide whether to port selective Sourik marketing widgets or keep mainline Catalyst as the only truth. |
|
||||||
|
| `W1-21` | Python FastAPI server with PostgreSQL | `Done` | Implemented and live. | Closed for Sprint 1 baseline. |
|
||||||
|
| `W1-22` | `/api/leads`, `/api/chat-logs` for Oracle | `Missing` | Mainline still lacks real mounted implementations here, and `routes_crm.py` / `routes_oracle.py` are zero-byte placeholders. | Strong candidate to import selectively from Sourik and adapt to the mainline DB contract. |
|
||||||
|
| `W1-23` | `/api/biometrics`, `/api/sentiment` endpoints for Sentinel | `Changed` | Current mainline uses Sentinel websocket/session architecture instead of this exact REST shape, but there is a real working browser-webcam perception path using MediaPipe and WebSocket packet streaming. | Functional equivalent exists; the next step is live validation from Sayan's MacBook rather than re-arguing endpoint names. |
|
||||||
|
| `W1-24` | WebSockets to stream real-time updates to WebOS | `Partial` | Sentinel and notification live flows exist, including QD update broadcasts back into the perception player. | Broader CRM websocket sync is not fully closed. |
|
||||||
|
| `W2-26` | Connect React frontend components to FastAPI | `Partial` | Mainline has live backend wiring in some modules, but the CRM surface still depends on mocks and incomplete contracts. | Replace mock data, land CRM/oracle routes, and verify contract parity end-to-end. |
|
||||||
|
| `W2-27` | Simplified Kanban CRM pipeline | `Missing` | The polished implementation exists in Sourik, not in current mainline. | Important gap and likely one of the first selective imports. |
|
||||||
|
| `W2-28` | Dashboard visualizes AI sentiment output | `Partial` | Sentinel/dashboard UI work exists. | Needs a stricter acceptance pass tied to real CRM data and post-tour actions. |
|
||||||
|
| `W1-30` | Deploy OpenClaw as primary communication agent | `Changed` | The target concept has shifted to NemoClaw as the future operating agent, not OpenClaw as originally phrased. | Define the actual v1 NemoClaw contract and keep the model-local migration in v2. |
|
||||||
|
| `W1-31` | Connect bot to WhatsApp/Email APIs | `V2` | Not current mainline truth. | Can return when communication agent architecture is stabilized. |
|
||||||
|
| `W1-32` | Configure DM pairing and security allowlists | `V2` | More agent-stack specific than current core product need. | Defer. |
|
||||||
|
| `W1-33` | Route parsed transcripts/call durations into CRM DB | `Partial` | Architectural direction exists. | Concrete integrated mainline path is still incomplete. |
|
||||||
|
| `W1-35` | Set up MCP server | `V2` | Not part of current mainline critical path. | Defer. |
|
||||||
|
| `W1-36` | Configure HEARTBEAT or Cron background tasks | `Partial` | Infra timers/services now exist for ops sync and auto-heal. | Agent heartbeat specifically is not mainline yet. |
|
||||||
|
| `W1-37` | Configure Brave Search API for autonomous research | `V2` | Exists only in parallel/agent-oriented direction. | Defer. |
|
||||||
|
| `W2-39` | Integrate Meta Business API and Google Ads API as skills | `Partial` | Meta exists in `routes_catalyst.py`; Google Ads is not landed in mainline. | Keep Google as open and treat the current state as Meta-first. |
|
||||||
|
| `W2-40` | Read insights, manage budgets, execute bidding | `Partial` | Mainline covers campaign creation, creative sync, realtime insights, and lookalikes. | Budget governance, automated bidding, and unified marketing state are still incomplete. |
|
||||||
|
| `W2-41` | Bridge agent to Sagnik ComfyUI API for posters/videos | `Partial` | The gateway/queue model exists, but the agent-operable bridge is not yet a polished first-class mainline subsystem. | Candidate for consolidation once NemoClaw/Catalyst boundaries are clarified. |
|
||||||
|
| `W2-42` | Autonomous content posting via headless browser or APIs | `V2` | Not done in mainline. | Defer. |
|
||||||
|
| `44` | Future Life Simulation workflow | `V2` | The story is intentionally pushed to v2 now. Wan workflow assets exist, but not the full Future Life product layer. | Keep out of Sprint 1 closure. |
|
||||||
|
| `45` | Time & Light Engine in Swift iPad app | `Partial` | The underlying implementation is materially there: AR sun overlay, sun math, SceneKit lighting, simulator path. | Finish product hardening and real-device verification. |
|
||||||
|
| `46` | Touch sliders for month/time/obstruction | `Partial` | Slider-driven control exists in the iOS inventory/time-light surface. | Tighten UX and confirm the final obstruction/massing behavior you want. |
|
||||||
|
| `48` | Legacy Mode wealth projection UI | `V2` | Not in current mainline. | Good v2 candidate. |
|
||||||
|
| `49` | Social Proof live map | `V2` | Not closed in mainline. | Good v2 candidate. |
|
||||||
|
| `50` | Sentinel backend for eye-tracking and micro-expression from iPad camera | `Partial` | This capability exists more clearly in Sourik's parallel Sentinel work than in current mainline. Mainline has the Sentinel backbone, but not the final iPad-first ingestion path you want to test. | Decide the testing source path: iPad camera, browser instrumentation, or public/free CCTV footage for development. |
|
||||||
|
| `51` | Dashboard visualize emotional spike by room | `Partial` | The data concept exists, but the sales-facing room-peak UI payoff is not clearly delivered in mainline. | Build the actual post-tour anchor surface, not just the data plumbing. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Is Actually Closed in Sprint 1 Terms
|
||||||
|
|
||||||
|
These items are reasonably closed if judged by evolved product reality rather than literal wording:
|
||||||
|
|
||||||
|
- native app shell exists
|
||||||
|
- Dream Weaver local workflow stack exists
|
||||||
|
- Wan 2.2 and Qwen workflow assets exist locally
|
||||||
|
- FastAPI + PostgreSQL neural core exists
|
||||||
|
- Sentinel backbone exists
|
||||||
|
- browser-webcam Sentinel perception path exists
|
||||||
|
- vault/notifications exist
|
||||||
|
- scenes/video/session pipeline exists
|
||||||
|
- live AWS/Linux/ingress infrastructure exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Is Escaping Attention
|
||||||
|
|
||||||
|
These are the most likely to be forgotten because adjacent systems exist:
|
||||||
|
|
||||||
|
1. `leads` and `chat-logs` APIs
|
||||||
|
- easy to assume done because backend exists
|
||||||
|
- they are not landed in current mainline routes yet
|
||||||
|
|
||||||
|
2. Kanban CRM logic
|
||||||
|
- same issue
|
||||||
|
|
||||||
|
3. camera-to-Comfy flow in the iPad app
|
||||||
|
- implementation exists, but endpoint stability and integration need proving after merge
|
||||||
|
|
||||||
|
4. marketing execution path
|
||||||
|
- there is a substantial Catalyst module, but the backend and execution loop are not fully unified
|
||||||
|
|
||||||
|
5. room-level emotional spike visualization
|
||||||
|
- scene-aware data exists
|
||||||
|
- explicit sales UI payoff may still be missing
|
||||||
|
|
||||||
|
6. prompt inventory
|
||||||
|
- prompt files exist in multiple places
|
||||||
|
- there is no single canonical map of persona/function -> prompt -> model -> API surface
|
||||||
|
|
||||||
|
7. live MacBook webcam validation
|
||||||
|
- the path exists in code
|
||||||
|
- it still needs a real end-to-end run from Sayan's machine against the current deployed environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Interpretation for Planning
|
||||||
|
|
||||||
|
### Keep in active near-term scope
|
||||||
|
|
||||||
|
- leads API
|
||||||
|
- chat logs API
|
||||||
|
- kanban pipeline
|
||||||
|
- frontend/backend CRM wiring
|
||||||
|
- CRM database structure
|
||||||
|
- synthetic `100`-client dataset for system verification
|
||||||
|
- endpoint stabilization for the iPad app
|
||||||
|
- room-level emotional spike sales surface
|
||||||
|
- marketing execution path consolidation
|
||||||
|
- prompt inventory and persona contract mapping
|
||||||
|
|
||||||
|
### Treat as explicitly v2
|
||||||
|
|
||||||
|
- MCP server and Brave search
|
||||||
|
- autonomous content posting
|
||||||
|
- Future Life cinematic product layer
|
||||||
|
- Legacy Mode wealth projection
|
||||||
|
- Social Proof live clustering layer
|
||||||
|
|
||||||
|
### Treat as changed, not failed
|
||||||
|
|
||||||
|
- 8xA100 provisioning and node virtualization
|
||||||
|
- SSH-tunnel-based access model
|
||||||
|
- OpenClaw-primary communication architecture
|
||||||
|
- combined Future Life + Time & Light story packaging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRM Remaining Pieces
|
||||||
|
|
||||||
|
These are the concrete mainline CRM gaps visible in the repo today:
|
||||||
|
|
||||||
|
1. Backend lead and chat routes are not landed.
|
||||||
|
- `backend/api/routes_crm.py` is zero bytes
|
||||||
|
- `backend/api/routes_oracle.py` is zero bytes
|
||||||
|
|
||||||
|
2. Frontend CRM still depends on mock data.
|
||||||
|
- `app/src/components/oracle/mockLeads.ts` is still the effective lead source for parts of the UI
|
||||||
|
|
||||||
|
3. Kanban is not implemented in mainline.
|
||||||
|
- The cleanest working version currently lives in Sourik's tree
|
||||||
|
|
||||||
|
4. Chat transcript and interaction ingestion are not unified into one canonical CRM schema yet.
|
||||||
|
|
||||||
|
5. Oracle action surfaces and CRM writebacks are not yet a stable contract.
|
||||||
|
|
||||||
|
6. Websocket semantics are not yet closed for CRM pipeline movement, lead updates, and interaction feeds.
|
||||||
|
|
||||||
|
7. Synthetic verification data does not exist yet.
|
||||||
|
- You should generate at least `100` realistic synthetic client/lead records and run the full pipeline against them before treating CRM as stable.
|
||||||
|
|
||||||
|
## CRM Database Brainstorming Starter
|
||||||
|
|
||||||
|
Minimum canonical entities for the next pass:
|
||||||
|
|
||||||
|
- `projects`
|
||||||
|
- `properties`
|
||||||
|
- `units`
|
||||||
|
- `leads`
|
||||||
|
- `lead_contact_methods`
|
||||||
|
- `lead_interactions`
|
||||||
|
- `lead_messages`
|
||||||
|
- `lead_stage_events`
|
||||||
|
- `site_visits`
|
||||||
|
- `sentinel_sessions`
|
||||||
|
- `sentiment_events`
|
||||||
|
- `room_interest_events`
|
||||||
|
- `tasks`
|
||||||
|
- `owners/users`
|
||||||
|
- `campaign_attributions`
|
||||||
|
- `documents`
|
||||||
|
- `oracle_actions`
|
||||||
|
|
||||||
|
High-value relationships:
|
||||||
|
|
||||||
|
- one `project` has many `properties`
|
||||||
|
- one `property` has many `units`
|
||||||
|
- one `lead` can have many `interactions`, `messages`, `site_visits`, and `stage_events`
|
||||||
|
- one `sentinel_session` can produce many `sentiment_events` and `room_interest_events`
|
||||||
|
- one `oracle_action` should map back to one canonical CRM write event
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt Inventory Gap
|
||||||
|
|
||||||
|
Prompt work exists, but the system does not yet have one canonical inventory. At minimum the next prompt pass should answer:
|
||||||
|
|
||||||
|
1. Which personas exist now:
|
||||||
|
- Oracle
|
||||||
|
- NemoClaw
|
||||||
|
- Catalyst
|
||||||
|
- Sentinel/QD evaluator
|
||||||
|
- CCTV profiler
|
||||||
|
- lead tagger
|
||||||
|
|
||||||
|
2. Which files are canonical:
|
||||||
|
- `backend/nemoclaw_prompts/qd_calculator.md`
|
||||||
|
- `backend/nemoclaw_prompts/lead_tagger.md`
|
||||||
|
- `backend/nemoclaw_prompts/cctv_profiler.md`
|
||||||
|
- Dream Weaver prompt files under `comfy_engine/prompts/`
|
||||||
|
- any Oracle/Catalyst prompt files that still only exist in docs or parallel code
|
||||||
|
|
||||||
|
3. Which model each prompt targets
|
||||||
|
|
||||||
|
4. Which runtime/API surface calls each prompt
|
||||||
|
|
||||||
|
5. Which prompts are system prompts, tool prompts, UI copy, or generation prompts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updated Active Task List
|
||||||
|
|
||||||
|
### Must finish for Sprint 1 truth to feel real
|
||||||
|
|
||||||
|
- land canonical `leads`, `chat_logs`, and `kanban` backend routes
|
||||||
|
- replace frontend CRM mock data with live API-backed state
|
||||||
|
- define and implement the canonical CRM database schema
|
||||||
|
- generate `100` synthetic client/lead records and verify the end-to-end CRM flow
|
||||||
|
- validate the Sentinel live-session webcam path from Sayan's MacBook
|
||||||
|
- stabilize iOS endpoint passthrough and prove camera-to-Dream-Weaver again
|
||||||
|
- finish the room-level emotional spike sales surface
|
||||||
|
- create the prompt inventory and persona/function map
|
||||||
|
|
||||||
|
### Important, but after the CRM spine is stable
|
||||||
|
|
||||||
|
- unify Dream Weaver/Qwen/Wan async automation into one canonical queue API
|
||||||
|
- harden Catalyst execution flow from UI -> backend -> generation -> asset sync
|
||||||
|
- define the v1 NemoClaw contract inside the product
|
||||||
|
- decide which Sourik CRM/marketing modules are selectively imported
|
||||||
|
|
||||||
|
### Explicitly v2
|
||||||
|
|
||||||
|
- Future Life cinematic product layer
|
||||||
|
- MCP server integration
|
||||||
|
- Brave Search autonomous research
|
||||||
|
- autonomous content posting
|
||||||
|
- Legacy Mode wealth projection
|
||||||
|
- Social Proof live clustering layer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bottom Line
|
||||||
|
|
||||||
|
Sprint 1 is best described as:
|
||||||
|
|
||||||
|
- **infrastructure and core runtime: substantially achieved**
|
||||||
|
- **Dream Weaver / Qwen / Wan local workflow assets: materially present**
|
||||||
|
- **Sentinel backbone: substantially achieved**
|
||||||
|
- **web and iOS implementation surface: materially present**
|
||||||
|
- **full CRM operationalization: still incomplete**
|
||||||
|
- **agent, marketing, and prompt unification: incomplete and needs explicit ownership**
|
||||||
|
|
||||||
|
The biggest remaining work is not more infra.
|
||||||
|
|
||||||
|
It is:
|
||||||
|
|
||||||
|
- completing the CRM layer
|
||||||
|
- deciding the canonical CRM schema
|
||||||
|
- stabilizing app/backend passthrough
|
||||||
|
- unifying frontend/backend contracts
|
||||||
|
- building a prompt inventory from first principles
|
||||||
|
- deciding which Sourik subsystems to port into mainline
|
||||||
|
- explicitly labeling v2 items so they stop pretending to be Sprint 1 leftovers
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# NemoClaw Setup Truth
|
# NemoClaw Setup Truth
|
||||||
|
|
||||||
Updated: April 2, 2026
|
Updated: April 12, 2026
|
||||||
|
|
||||||
## 1. Purpose
|
## 1. Purpose
|
||||||
|
|
||||||
@@ -10,15 +10,24 @@ This is not the original intended architecture. This is the current operational
|
|||||||
|
|
||||||
## 2. High-Level Summary
|
## 2. High-Level Summary
|
||||||
|
|
||||||
Project Velocity uses the term "NemoClaw" for the reasoning and prompt layer attached to the Sentinel QD Engine. In practice, this is now split into two different concerns:
|
Project Velocity uses the term "NemoClaw" for the reasoning and prompt layer attached to the Sentinel QD Engine. In practice, this is now split into three different concerns:
|
||||||
|
|
||||||
1. Prompted reasoning used by the FastAPI backend
|
1. Prompted reasoning used by the FastAPI backend
|
||||||
2. OpenShell / gateway infrastructure that remains installed on the AWS node
|
2. OpenShell / gateway infrastructure that remains installed on the AWS node
|
||||||
|
3. Python-native append layers used by Oracle planning, MCP-style tool registration, and workflow dispatch preview
|
||||||
|
|
||||||
The active FastAPI inference path is NVIDIA-hosted OpenAI-compatible chat completions.
|
The active FastAPI inference path is NVIDIA-hosted OpenAI-compatible chat completions.
|
||||||
|
|
||||||
The OpenShell gateway and Ollama are still installed and running as adjacent infrastructure, but they are not the active primary scoring path used by `backend/services/nemoclaw_client.py`.
|
The OpenShell gateway and Ollama are still installed and running as adjacent infrastructure, but they are not the active primary scoring path used by `backend/services/nemoclaw_client.py`.
|
||||||
|
|
||||||
|
The root codebase now also includes Python-native compatibility layers inspired by Sourik's Go runtime:
|
||||||
|
|
||||||
|
- `backend/services/nemoclaw_runtime.py`
|
||||||
|
- `backend/services/mcp_registry.py`
|
||||||
|
- `backend/oracle/persona_service.py`
|
||||||
|
|
||||||
|
These append the current root without replacing the active NVIDIA-hosted inference path.
|
||||||
|
|
||||||
## 3. Node and Network Truth
|
## 3. Node and Network Truth
|
||||||
|
|
||||||
AWS region: `us-east-1`
|
AWS region: `us-east-1`
|
||||||
@@ -81,6 +90,24 @@ PostgreSQL 14 data directory.
|
|||||||
`backend/services/nemoclaw_client.py`
|
`backend/services/nemoclaw_client.py`
|
||||||
Primary reasoning client used by the FastAPI backend.
|
Primary reasoning client used by the FastAPI backend.
|
||||||
|
|
||||||
|
`backend/services/nemoclaw_runtime.py`
|
||||||
|
Python-native append layer for workflow dispatch planning, webhook verification, and claim-style helper behavior.
|
||||||
|
|
||||||
|
`backend/services/mcp_registry.py`
|
||||||
|
Python-native MCP/search tool registry append layer used by Oracle helper surfaces.
|
||||||
|
|
||||||
|
`backend/oracle/persona_service.py`
|
||||||
|
Subordinate Oracle persona planning layer that recommends component templates, renders prompt assets, and augments Oracle v1.
|
||||||
|
|
||||||
|
`backend/api/routes_crm.py`
|
||||||
|
Root PostgreSQL-first CRM append layer for `leads`, `chat_logs`, `kanban`, and analytics routes.
|
||||||
|
|
||||||
|
`backend/api/routes_oracle.py`
|
||||||
|
Root Oracle helper append layer for workflow preview and MCP tool discovery.
|
||||||
|
|
||||||
|
`backend/oracle/router_v1.py`
|
||||||
|
Mounted Oracle v1 API surface for canvas, prompts, persona helpers, and collaboration.
|
||||||
|
|
||||||
`backend/routers/videos.py`
|
`backend/routers/videos.py`
|
||||||
Marketing-video catalog endpoint for the Sentinel live-session picker.
|
Marketing-video catalog endpoint for the Sentinel live-session picker.
|
||||||
|
|
||||||
@@ -182,6 +209,28 @@ No longer the primary path for backend scoring.
|
|||||||
5. The backend calls NVIDIA hosted completions using `nvidia/nemotron-3-super-120b-a12b`
|
5. The backend calls NVIDIA hosted completions using `nvidia/nemotron-3-super-120b-a12b`
|
||||||
6. The result updates QD score state and is broadcast back over WebSocket
|
6. The result updates QD score state and is broadcast back over WebSocket
|
||||||
|
|
||||||
|
### Current Oracle canvas planning append flow
|
||||||
|
|
||||||
|
1. Frontend can call `/api/oracle/v1/canvas-pages/{pageId}/prompts`
|
||||||
|
2. `backend/oracle/prompt_orchestrator.py` builds a retrieval plan
|
||||||
|
3. `backend/oracle/persona_service.py` recommends reusable component templates and emits a planning note block
|
||||||
|
4. `backend/services/nemoclaw_runtime.py` produces a workflow dispatch preview for ComfyUI-backed execution
|
||||||
|
5. `backend/oracle/data_access_gateway.py` runs only whitelisted PostgreSQL queries
|
||||||
|
6. Oracle commits the resulting components into the active canvas revision
|
||||||
|
|
||||||
|
### Current CRM and analytics append flow
|
||||||
|
|
||||||
|
1. Root FastAPI mounts `backend/api/routes_crm.py`
|
||||||
|
2. Canonical root endpoints now exist for:
|
||||||
|
- `/api/leads`
|
||||||
|
- `/api/leads/demographics`
|
||||||
|
- `/api/chat-logs`
|
||||||
|
- `/api/kanban/board`
|
||||||
|
- `/api/kanban/move`
|
||||||
|
- `/api/analytics/sentiment-scatter`
|
||||||
|
3. These routes use the root asyncpg pool and PostgreSQL-first storage contract
|
||||||
|
4. CRM WebSocket sync is still intentionally deferred
|
||||||
|
|
||||||
### Current lead-tagging flow
|
### Current lead-tagging flow
|
||||||
|
|
||||||
1. Broker or system calls `/api/sentinel/tag-lead`
|
1. Broker or system calls `/api/sentinel/tag-lead`
|
||||||
@@ -232,6 +281,22 @@ Why it still exists:
|
|||||||
|
|
||||||
What it is not:
|
What it is not:
|
||||||
- It is not the current primary inference path for backend scoring
|
- It is not the current primary inference path for backend scoring
|
||||||
|
- It is not the root source of truth for Oracle or CRM orchestration
|
||||||
|
|
||||||
|
## 8.5 Python-Native Append Responsibilities
|
||||||
|
|
||||||
|
These are now part of root truth:
|
||||||
|
|
||||||
|
- Oracle persona prompt loading and render helpers live in Python, not Go
|
||||||
|
- MCP/search registration lives in Python, not Go
|
||||||
|
- Workflow dispatch planning for Oracle-to-Comfy orchestration lives in Python, not Go
|
||||||
|
- Claim-style helper behavior is appended in Python as a compatibility layer, not as a second backend center
|
||||||
|
|
||||||
|
What remains deferred:
|
||||||
|
|
||||||
|
- Full production webhook runtime parity with Sourik's Go stack
|
||||||
|
- Full external search provider execution inside the MCP layer
|
||||||
|
- Autonomous posting and non-root agent/webhook services
|
||||||
|
|
||||||
## 9. Prompts
|
## 9. Prompts
|
||||||
|
|
||||||
|
|||||||
41
.Agent Context/Sprint 1/tasks.csv
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
id,ref,subject,description,user_story,sprint_id,sprint,sprint_estimated_start,sprint_estimated_finish,owner,owner_full_name,assigned_to,assigned_to_full_name,status,is_iocaine,is_closed,us_order,taskboard_order,attachments,external_reference,tags,watchers,voters,created_date,modified_date,finished_date,due_date,due_date_reason
|
||||||
|
1,2,"W1: Define the local ""Black Box"" edge server requirements for the offline-first experience center setup.",,1,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221706761,0,0,,,[],0,2026-02-27 19:48:26.771342+00:00,2026-02-28 18:47:00.119354+00:00,2026-02-28 18:47:00.120611+00:00,,
|
||||||
|
2,3,W2: Provision the AWS 8xA100 instance.,,1,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221725889,1,0,,,[],0,2026-02-27 19:48:45.899103+00:00,2026-03-09 18:10:45.526266+00:00,2026-03-09 18:10:45.527568+00:00,,
|
||||||
|
3,4,W2: Configure virtualization to split the AWS instance into two compute nodes: Node 1 (Sourik's Agent/Bot Operations) and Node 2 (Sagnik & Sayan's Model/Render Operations).,,1,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221741690,2,0,,,[],0,2026-02-27 19:49:01.700573+00:00,2026-03-09 18:10:16.322481+00:00,2026-03-09 18:10:16.323670+00:00,,
|
||||||
|
4,5,W2: Set up secure SSH tunnels networks to allow remote access to the AWS nodes.,,1,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221755869,3,0,,,[],0,2026-02-27 19:49:15.879374+00:00,2026-03-09 18:10:51.657474+00:00,2026-03-09 18:10:51.658742+00:00,,
|
||||||
|
5,7,"W1: Build the ""Dream Weaver"" interior restyling workflow using ControlNet + segment masking to preserve room geometry while changing aesthetics.",,6,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,True,True,1772221838514,0,0,,,[5],0,2026-02-27 19:50:38.524388+00:00,2026-03-09 18:08:05.779876+00:00,2026-03-09 18:08:05.781212+00:00,,
|
||||||
|
6,8,W1: Build a marketing poster generation workflow using Qwen-Image 2512 to leverage its advanced multilingual text rendering capabilities for precise real estate typography.,,6,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221850050,1,0,,,[],0,2026-02-27 19:50:50.060934+00:00,2026-03-24 07:52:12.691412+00:00,2026-03-24 07:52:12.695999+00:00,,
|
||||||
|
7,9,W2: Implement the Wan 2.2 (14B or 1.3B) video generation workflow for cinematic promotional videos.,,6,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221863123,2,0,,,[],0,2026-02-27 19:51:03.133310+00:00,2026-04-12 09:44:11.404656+00:00,2026-04-12 09:44:11.406137+00:00,,
|
||||||
|
8,10,W2: Expose all ComfyUI workflows via the Asynchronous Queue API so Sourik's agents can trigger them automatically.,,6,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221874697,3,0,,,[],0,2026-02-27 19:51:14.706589+00:00,2026-04-12 09:44:17.813778+00:00,2026-04-12 09:44:17.815079+00:00,,
|
||||||
|
9,12,"W1: Draft ""The Oracle"" persona prompts (adapting the tone of top-tier Dubai brokers) for the WhatsApp CRM agent.",,11,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,0,1,0,,,[],0,2026-02-27 19:52:17.829687+00:00,2026-04-12 09:44:34.497116+00:00,2026-04-12 09:44:34.498417+00:00,,
|
||||||
|
10,13,"W1: Create marketing strategy prompts for ""The Catalyst"" to generate Meta/Google ad copy based on demographic inputs.",,11,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221937822,2,0,,,[],0,2026-02-27 19:52:32.110749+00:00,2026-04-12 09:44:32.953327+00:00,2026-04-12 09:44:32.954497+00:00,,
|
||||||
|
11,14,"W1: Lock the frontend UI design (the ""Apple/Steve Jobs"" aesthetic) and officially hand over the React components and required API schemas to Sayan for backend wiring.",,11,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,1772221937821,0,0,,,[],0,2026-02-27 19:52:45.221808+00:00,2026-03-24 07:52:58.958801+00:00,2026-03-24 07:52:58.960593+00:00,,
|
||||||
|
12,16,"W1: Build the native SwiftUI app shell mirroring the WebOS interface (Dashboard, Inventory, Oracle tabs).",,15,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222024957,0,6,,,[6],0,2026-02-27 19:53:44.966875+00:00,2026-03-07 12:36:31.475097+00:00,2026-03-07 12:36:31.476262+00:00,,
|
||||||
|
13,17,W1: Implement the camera capture feature to take photos of empty walls/rooms and push them to Sagnik's ComfyUI API endpoint.,,15,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222036382,1,0,,,[],0,2026-02-27 19:53:56.392802+00:00,2026-03-24 07:55:47.038974+00:00,2026-03-24 07:55:47.040142+00:00,,
|
||||||
|
14,18,W1: Integrate ARKit/CoreLocation/CoreMotion to overlay the mathematical Sun Path over the live camera feed or 3D model view.,,15,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222050984,2,0,,,[],0,2026-02-27 19:54:10.993883+00:00,2026-03-24 07:55:49.152302+00:00,2026-03-24 07:55:49.153709+00:00,,
|
||||||
|
15,20,W1: Set up the Marketing page frontend for Sourik.,,19,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222108515,0,0,,,[],0,2026-02-27 19:55:08.524248+00:00,2026-03-24 07:53:40.935245+00:00,2026-03-24 07:53:40.936625+00:00,,
|
||||||
|
16,21,W1: Set up the Python FastAPI server with a PostgreSQL database.,,19,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222119363,1,0,,,[],0,2026-02-27 19:55:19.373616+00:00,2026-04-12 09:44:42.960490+00:00,2026-04-12 09:44:42.961685+00:00,,
|
||||||
|
17,22,"W1: Create API endpoints for ""The Oracle"" (/api/leads, /api/chat-logs) to receive data from Sourik's WhatsApp bots.",,19,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222129398,2,0,,,[],0,2026-02-27 19:55:29.408246+00:00,2026-04-12 09:44:47.034107+00:00,2026-04-12 09:44:47.035298+00:00,,
|
||||||
|
18,23,"W1: Create API endpoints for ""The Sentinel"" (/api/biometrics, /api/sentiment) to ingest video player facial/voice data points.",,19,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222140319,3,0,,,[],0,2026-02-27 19:55:40.327197+00:00,2026-04-12 09:44:53.214983+00:00,2026-04-12 09:44:53.216156+00:00,,
|
||||||
|
19,24,W1: Set up WebSockets to stream real-time updates directly to the WebOS React frontend.,,19,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222152434,4,0,,,[],0,2026-02-27 19:55:52.443171+00:00,2026-04-12 09:45:01.241831+00:00,2026-04-12 09:45:01.243037+00:00,,
|
||||||
|
20,26,W2: Connect the frontend React components to the FastAPI endpoints.,,25,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222216251,0,0,,,[],0,2026-02-27 19:56:56.261160+00:00,2026-04-12 09:45:04.249760+00:00,2026-04-12 09:45:04.251615+00:00,,
|
||||||
|
21,27,"W2: Develop the logic for the simplified ""Kanban"" CRM pipeline, ensuring lead stages automatically update based on triggers from ""The Oracle"".",,25,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222226063,1,0,,,[],0,2026-02-27 19:57:06.074849+00:00,2026-04-12 09:45:06.556812+00:00,2026-04-12 09:45:06.557988+00:00,,
|
||||||
|
22,28,W2: Ensure the WebOS dashboard accurately visualizes the parsed AI sentiment data’s output.,,25,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772222236473,2,0,,,[],0,2026-02-27 19:57:16.484481+00:00,2026-04-12 09:45:08.647867+00:00,2026-04-12 09:45:08.649243+00:00,,
|
||||||
|
23,30,W1: Deploy OpenClaw to act as the primary communication agent.,,29,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222353354,0,0,,,[],0,2026-02-27 19:59:13.363801+00:00,2026-03-24 07:56:23.466079+00:00,2026-03-24 07:56:23.467239+00:00,,
|
||||||
|
24,31,W1: Connect the bot to WhatsApp/Email APIs to ingest client messages.,,29,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222364232,1,0,,,[],0,2026-02-27 19:59:24.241508+00:00,2026-03-24 07:56:25.188356+00:00,2026-03-24 07:56:25.189555+00:00,,
|
||||||
|
25,32,W1: Configure DM pairing and security allowlists.,,29,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222376473,2,0,,,[],0,2026-02-27 19:59:36.483694+00:00,2026-03-24 07:56:27.169875+00:00,2026-03-24 07:56:27.171056+00:00,,
|
||||||
|
26,33,"W1: Route all parsed chat transcripts, call durations, and interaction logs directly into Sayan's CRM database via FastAPI webhooks.",,29,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222387241,3,0,,,"[5, 6]",0,2026-02-27 19:59:47.252349+00:00,2026-03-24 07:56:30.206587+00:00,2026-03-24 07:56:30.207755+00:00,,
|
||||||
|
27,35,"W1: Set up the Model Context Protocol (MCP) server for secure access to local files, the property database, and the internet.",,34,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222448792,0,0,,,[],0,2026-02-27 20:00:48.802138+00:00,2026-03-24 07:56:37.925832+00:00,2026-03-24 07:56:37.927003+00:00,,
|
||||||
|
28,36,W1: Configure HEARTBEAT.md or Cron tools for periodic background tasks.,,34,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222458999,1,0,,,[],0,2026-02-27 20:00:59.008938+00:00,2026-03-24 07:56:40.173506+00:00,2026-03-24 07:56:40.174676+00:00,,
|
||||||
|
29,37,W1: Configure Brave Search API to allow the agent to autonomously research target audiences.,"Task #37 is complete! We built the Brave Search tool into the central MCP server. It's fully functional with exponential backoff for rate limits, context-aware timeouts, and a graceful fallback in the event of missing keys (automatically reverts back to DuckDuckGo, so we never stall). All tests pass. Will just need to grab the standard free API Key when we move to cloud.",34,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222469781,2,0,,,[],0,2026-02-27 20:01:09.790996+00:00,2026-03-24 07:56:42.200997+00:00,2026-03-24 07:56:42.202158+00:00,,
|
||||||
|
30,39,"W2: Integrate Meta Business API and Google AdWords API as agent ""Skills"".",,38,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222528624,1,0,,,[],0,2026-02-27 20:02:08.634609+00:00,2026-04-12 09:45:13.092812+00:00,2026-04-12 09:45:13.094098+00:00,,
|
||||||
|
31,40,"W2: Give the agent the ability to read ad insights, manage marketing budgets, and execute automated bidding strategies.",,38,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222537734,2,0,,,[],0,2026-02-27 20:02:17.744121+00:00,2026-04-12 09:45:19.901845+00:00,2026-03-24 07:57:18.950521+00:00,,
|
||||||
|
32,41,W2: Write the bridging script allowing the agent to autonomously prompt Sagnik's ComfyUI API to generate custom posters and promotional videos.,,38,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222546643,3,0,,,[],0,2026-02-27 20:02:26.650336+00:00,2026-04-12 09:45:15.334261+00:00,2026-04-12 09:45:15.335454+00:00,,
|
||||||
|
33,42,W2: Configure the headless browser tool or social APIs for autonomous content posting.,,38,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772222537735,4,0,,,[],0,2026-02-27 20:02:40.625582+00:00,2026-03-24 07:57:23.338020+00:00,2026-03-24 07:57:21.742863+00:00,,
|
||||||
|
34,44,"Build a ComfyUI/Wan 2.2 workflow for ""Future Life Simulation"" that generates cinematic videos of specific lifestyle prompts (morning sunlight, kids playing, dinner parties) mapped to the unit's floorplan.",,43,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,Closed,False,True,0,0,0,,,[],0,2026-02-28 12:39:21.287833+00:00,2026-04-12 09:45:41.029569+00:00,2026-04-12 09:45:41.030850+00:00,,
|
||||||
|
35,45,"Integrate a ""Time & Light Engine"" into the Swift iPad app using ARKit/SceneKit to simulate real-time sun paths, seasonal shadows, and weather changes (rain, festive lighting) over the 3D model.",,43,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772282361281,1,0,,,[],0,2026-02-28 12:39:39.275027+00:00,2026-04-12 09:45:43.695076+00:00,2026-04-12 09:45:43.696260+00:00,,
|
||||||
|
36,46,"Add interactive touchscreen sliders to the iPad app to control month, time of day, and view obstruction massing.",,43,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772282437251,2,0,,,[],0,2026-02-28 12:40:37.257926+00:00,2026-04-12 09:45:48.208124+00:00,2026-04-12 09:45:48.209326+00:00,,
|
||||||
|
37,48,"Build the ""Legacy Mode"" wealth projection UI in the iPad app and WebOS, visualizing 10-20 year compounding appreciation and rental yields against gold/stock benchmarks.",,47,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772282707104,0,0,,,[],0,2026-02-28 12:45:07.113994+00:00,2026-04-12 09:45:56.516622+00:00,2026-04-12 09:45:56.517875+00:00,,
|
||||||
|
38,49,"Create the ""Social Proof"" live map in the frontend, dynamically clustering anonymized buyer demographics (NRI vs local, professions) to build tribe psychology.",,47,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772282719217,1,0,,,[],0,2026-02-28 12:45:19.226139+00:00,2026-04-12 09:45:59.591950+00:00,2026-04-12 09:45:59.593490+00:00,,
|
||||||
|
39,50,"Configure ""The Sentinel"" backend API to ingest and process eye-tracking and micro-expression data from the iPad's front-facing camera (with consent) during the tour.",Task #50 is complete! [sentinel.py](cci:7://file:///c:/MY%20VEL%20FILE/velocity/api/sentinel.py:0:0-0:0) is now extended. We set up 6 dedicated API endpoints ready to cleanly digest your real-time Eye-Tracking data alongside the Micro-Expression FACS streams. Everything is guarded strictly by an active-consent barrier and will automatically GDPR-purge dynamically on consent withdrawal. We can get together later this week and hit the endpoints to lock up iPad validation natively! Let me know when ready.,47,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,Closed,False,True,1772282731186,2,0,,,[],0,2026-02-28 12:45:31.195687+00:00,2026-04-12 09:45:54.625387+00:00,2026-04-12 09:45:54.627021+00:00,,
|
||||||
|
40,51,"Update the WebOS CRM dashboard to visualize the emotional spike data, highlighting exactly which rooms peaked the buyer's interest for post-tour sales anchoring.",,47,2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,Closed,False,True,1772282741695,3,0,,,[],0,2026-02-28 12:45:41.704928+00:00,2026-04-12 09:46:06.085593+00:00,2026-04-12 09:46:06.086765+00:00,,
|
||||||
|
12
.Agent Context/Sprint 1/userstories.csv
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
id,ref,subject,description,sprint_id,sprint,sprint_estimated_start,sprint_estimated_finish,owner,owner_full_name,assigned_to,assigned_to_full_name,assigned_users,assigned_users_full_name,status,is_closed,swimlane,back-points,design-points,front-points,ux-points,total-points,backlog_order,sprint_order,kanban_order,created_date,modified_date,finish_date,client_requirement,team_requirement,attachments,generated_from_issue,generated_from_task,from_task_ref,external_reference,tasks,tags,watchers,voters,due_date,due_date_reason,epics
|
||||||
|
17,1,1.1: Local/Cloud Hardware Config,"As an Architect, I need to configure the local and cloud hardware environments so the team can build without bottlenecks.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,sagnik,Sagnik,New,True,,8.0,,,,8.0,1772221676403176,1,1772221676413004,2026-02-27 19:47:56.413009+00:00,2026-02-27 20:03:07.048568+00:00,2026-03-09 18:10:51.665179+00:00,False,False,0,,,,,"2,3,4,5",,[],0,,,
|
||||||
|
18,6,1.2: ComfyUI Visual Workflows,"As an AI Visual Artist, I need to create API-ready ComfyUI workflows for ""The Catalyst"" and the ""Immersive Sales Companion"".",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,sagnik,Sagnik,New,True,,3.0,3.0,2.0,,8.0,1772221817549925,2,1772221817561352,2026-02-27 19:50:17.561358+00:00,2026-02-27 20:03:19.741415+00:00,2026-04-12 09:44:17.822335+00:00,False,False,0,,,,,"7,8,9,10",,[],0,,,
|
||||||
|
19,11,1.3: System Prompts & UI Logic,"As an AI Engineer, I need to generate system prompts and fine-tune models so ""The Oracle"" and ""The Catalyst"" behave like elite real estate professionals.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,sagnik,Sagnik,New,True,,,3.0,3.0,2.0,8.0,1772221917508059,3,1772221917519214,2026-02-27 19:51:57.519220+00:00,2026-02-27 20:03:30.481650+00:00,2026-04-12 09:44:34.504511+00:00,False,False,0,,,,,"12,13,14",,[],0,,,
|
||||||
|
20,15,2.1: Swift/iPad App,"As an iOS Developer, I need to build the ""Immersive Sales Companion"" iPad App using Swift.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,sayan,Sayan,New,True,,,2.0,3.0,3.0,8.0,1772222005626031,4,1772222005637374,2026-02-27 19:53:25.637380+00:00,2026-02-27 20:03:46.311976+00:00,2026-03-24 07:55:49.192166+00:00,False,False,0,,,,,"16,17,18",,[],0,,,
|
||||||
|
21,19,2.2: FastAPI Neural Core,"As a Backend Engineer, I need to build the FastAPI neural core to connect all 4 software components.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,sayan,Sayan,New,True,,8.0,,,,8.0,1772222093978956,5,1772222093990417,2026-02-27 19:54:53.990424+00:00,2026-02-27 20:03:54.338526+00:00,2026-04-12 09:45:01.249998+00:00,False,False,0,,,,,"20,21,22,23,24",,[],0,,,
|
||||||
|
22,25,2.3: CRM/WebOS React Wiring,"As a Full-Stack Engineer, I need to build the ""Walled Garden"" CRM and wire the React WebOS.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,sayan,Sayan,New,True,,3.0,,3.0,2.0,8.0,2,6,1772222201088772,2026-02-27 19:56:41.088779+00:00,2026-02-27 20:04:03.943036+00:00,2026-04-12 09:45:08.655385+00:00,False,False,0,,,,,"26,27,28",,[],0,,,
|
||||||
|
23,29,3.1: Claw Bot Ecosystem,"As an Automation Engineer, I need to deploy and manage the Claw bot ecosystem for ""The Oracle"".",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,sourik,Sourik,New,True,,5.0,,,,5.0,1772222326327364,7,1772222326336825,2026-02-27 19:58:46.336830+00:00,2026-02-27 20:04:13.817190+00:00,2026-03-24 07:56:30.213938+00:00,False,False,0,,,,,"30,31,32,33",,[],0,,,
|
||||||
|
24,34,3.2: MCP Server/Tools,"As an AI Operator, I need to set up the MCP Server and Agent System Tools.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,sourik,Sourik,New,True,,5.0,,,,5.0,1,8,1772222428317194,2026-02-27 20:00:28.317200+00:00,2026-02-27 20:04:22.989314+00:00,2026-03-24 07:56:42.208437+00:00,False,False,0,,,,,"35,36,37",,[],0,,,
|
||||||
|
25,38,3.3: Marketing Automation,"As a Marketing Automation Lead, I need to build ""The Catalyst"" integration.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sourik,Sourik,sourik,Sourik,New,True,,3.0,1.0,2.0,2.0,8.0,1772222512196395,9,1772222512207080,2026-02-27 20:01:52.207086+00:00,2026-02-27 20:04:31.646325+00:00,2026-04-12 09:45:15.341636+00:00,False,False,0,,,,,"39,40,41,42",,[],0,,,
|
||||||
|
26,43,4.1: Future Life and Time & Light Engine,"As an AI Visual Artist and iOS Developer, we need to build the ""Future Life"" and ""Time & Light"" engines to emotionally anchor the buyer to the property.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sagnik,Sagnik,"sagnik,sayan","Sagnik,Sayan",New,True,,8.0,5.0,5.0,5.0,23.0,1772282330939916,10,1772282330950456,2026-02-28 12:38:50.950461+00:00,2026-02-28 12:38:50.955347+00:00,2026-04-12 09:45:48.215061+00:00,False,False,0,,,,,"44,45,46",,[],0,,,
|
||||||
|
27,47,4.2: Engagement Intelligence and Social Proof layer,"As a Full-Stack Engineer and Automation Lead, we need to build the Engagement Intelligence and Social Proof layer to give the sales team data-driven closing tools.",2,Prototype Features Sprint,2026-02-28,2026-03-14,sagnik,Sagnik,sayan,Sayan,"sayan,sourik","Sayan,Sourik",New,True,,8.0,3.0,5.0,3.0,19.0,1772282692419979,11,1772282692429575,2026-02-28 12:44:52.429580+00:00,2026-02-28 12:44:52.435018+00:00,2026-04-12 09:46:06.092864+00:00,False,False,0,,,,,"48,49,50,51",,[],0,,,
|
||||||
|
BIN
Payload/Screenshots/01. Dashboard.png
Normal file
|
After Width: | Height: | Size: 448 KiB |
BIN
Payload/Screenshots/02. The Oracle.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
BIN
Payload/Screenshots/03-A. The Sentinel - Overview.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
Payload/Screenshots/03-B. The Sentinel - Live Session.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Payload/Screenshots/04. Inventory.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Payload/Screenshots/05. The Catalyst.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
Payload/Screenshots/06. Settings.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
253
README.md
@@ -1,3 +1,252 @@
|
|||||||
# Project_Velocity
|
# Project Velocity
|
||||||
|
|
||||||
Next-gen operating environment: WebOS and iPad App
|
Project Velocity is an on-prem real estate operating system for high-value property sales.
|
||||||
|
It combines a premium WebOS, an iPad field app, a FastAPI neural core, ComfyUI-based media generation, and biometric/sentiment-assisted sales intelligence.
|
||||||
|
|
||||||
|
This repository is the active product codebase for the Velocity suite.
|
||||||
|
|
||||||
|
## At A Glance
|
||||||
|
|
||||||
|
| Area | Current State |
|
||||||
|
| --- | --- |
|
||||||
|
| WebOS | Live UI shell with Dashboard, Oracle, Sentinel, Inventory, Catalyst, and Settings |
|
||||||
|
| Backend | Unified FastAPI backend with PostgreSQL, auth, Sentinel, Catalyst, CCTV, scenes, videos, and vault |
|
||||||
|
| iPad | Materially implemented app surface for inventory, camera flows, and Time & Light |
|
||||||
|
| AI Media | Dream Weaver, Qwen, and Wan workflow assets present locally |
|
||||||
|
| Infra | Linux control surface, AWS GPU workers, stable ingress, and S3-backed asset strategy |
|
||||||
|
|
||||||
|
## Product Surfaces
|
||||||
|
|
||||||
|
- `Velocity WebOS`
|
||||||
|
- operator console for sales, inventory, perception, and marketing
|
||||||
|
- `The Oracle`
|
||||||
|
- AI-assisted lead intelligence and analytical workspace
|
||||||
|
- `The Sentinel`
|
||||||
|
- live perception, sentiment, and showroom engagement tracking
|
||||||
|
- `Inventory / Time & Light`
|
||||||
|
- property inventory surfaces and light-analysis tooling
|
||||||
|
- `The Catalyst`
|
||||||
|
- marketing campaign orchestration and asset generation
|
||||||
|
- `iPad App`
|
||||||
|
- field-facing app for inventory, camera workflows, and time/light experience
|
||||||
|
- `Neural Core`
|
||||||
|
- unified FastAPI backend with PostgreSQL, auth, Sentinel, Catalyst, CCTV, scenes, videos, and vault services
|
||||||
|
|
||||||
|
## Product Screens
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### The Oracle
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### The Sentinel
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Inventory
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### The Catalyst
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
ipad[iPad App] --> webapi[FastAPI Neural Core]
|
||||||
|
webos[Velocity WebOS] --> webapi
|
||||||
|
webapi --> pg[(PostgreSQL)]
|
||||||
|
webapi --> sentinel[Sentinel Engine]
|
||||||
|
webapi --> catalyst[Catalyst Routes]
|
||||||
|
webapi --> vault[Vault Links]
|
||||||
|
webapi --> cctv[CCTV / Scene / Video Routers]
|
||||||
|
catalyst --> comfy[ComfyUI / Dream Weaver / Wan / Qwen]
|
||||||
|
comfy --> s3[S3 Model + Asset Store]
|
||||||
|
comfy --> gpu[AWS GPU Workers]
|
||||||
|
webos --> ingress[t4g Ingress]
|
||||||
|
ipad --> ingress
|
||||||
|
ingress --> webapi
|
||||||
|
ingress --> comfy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- `React 19`
|
||||||
|
- `Vite`
|
||||||
|
- `TypeScript`
|
||||||
|
- `Tailwind`
|
||||||
|
- `Framer Motion`
|
||||||
|
- `Recharts`
|
||||||
|
- `Zustand`
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- `FastAPI`
|
||||||
|
- `PostgreSQL`
|
||||||
|
- `WebSockets`
|
||||||
|
- `JWT auth`
|
||||||
|
- `asyncpg`
|
||||||
|
|
||||||
|
### AI / Media
|
||||||
|
|
||||||
|
- `ComfyUI`
|
||||||
|
- `Dream Weaver`
|
||||||
|
- `Qwen image workflows`
|
||||||
|
- `Wan 2.2 workflow assets`
|
||||||
|
- `Sentinel perception pipeline`
|
||||||
|
- `Google MediaPipe browser-side face landmarking`
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
|
||||||
|
- Linux control surface
|
||||||
|
- AWS GPU workers
|
||||||
|
- stable `t4g.micro` ingress
|
||||||
|
- S3-backed model and asset strategy
|
||||||
|
- route-managed Comfy exposure
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Web frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Default local URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:5173/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn backend.main:app --reload --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Health check:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Areas
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `app/` | Velocity WebOS frontend |
|
||||||
|
| `backend/` | FastAPI neural core |
|
||||||
|
| `iOS/` | native iPad application |
|
||||||
|
| `comfy_engine/` | Dream Weaver, Wan, Qwen workflow logic and assets |
|
||||||
|
| `infrastructure/` | ingress, ops, deployment, and recovery artifacts |
|
||||||
|
| `.Agent Context/` | working docs, bibels, strategy, and truth artifacts |
|
||||||
|
|
||||||
|
## Runtime Coverage
|
||||||
|
|
||||||
|
### WebOS modules
|
||||||
|
|
||||||
|
- Dashboard
|
||||||
|
- The Oracle
|
||||||
|
- The Sentinel
|
||||||
|
- Inventory
|
||||||
|
- The Catalyst
|
||||||
|
- Settings
|
||||||
|
|
||||||
|
### Backend capabilities already present
|
||||||
|
|
||||||
|
- authentication and token issuance
|
||||||
|
- Catalyst campaign routes
|
||||||
|
- Sentinel websocket/session ingestion
|
||||||
|
- CCTV routes
|
||||||
|
- scenes and videos routes
|
||||||
|
- vault routes
|
||||||
|
- static asset serving
|
||||||
|
- health checks
|
||||||
|
|
||||||
|
### Media and generation paths
|
||||||
|
|
||||||
|
- Dream Weaver workflow stack exists locally
|
||||||
|
- Qwen poster workflow assets exist locally
|
||||||
|
- Wan 2.2 workflow assets exist locally
|
||||||
|
- queue/gateway model exists and is being hardened into a canonical async path
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
Velocity is not built as a SaaS-first CRM.
|
||||||
|
It is designed as a private deployment system for builders, channel partners, and enterprise real-estate operators who want:
|
||||||
|
|
||||||
|
- private data ownership
|
||||||
|
- on-prem or client-controlled deployment
|
||||||
|
- AI-assisted sales acceleration
|
||||||
|
- perception-assisted showroom intelligence
|
||||||
|
- high-end marketing asset generation
|
||||||
|
- portfolio-aware expansion over time
|
||||||
|
|
||||||
|
## Current Delivery Status
|
||||||
|
|
||||||
|
Implemented now:
|
||||||
|
|
||||||
|
- strong WebOS UI shell
|
||||||
|
- real iOS implementation surface
|
||||||
|
- live FastAPI backend
|
||||||
|
- live ingress and AWS/Linux operational model
|
||||||
|
- Dream Weaver / Qwen / Wan workflow assets
|
||||||
|
- browser-webcam Sentinel path using MediaPipe
|
||||||
|
|
||||||
|
Still being closed:
|
||||||
|
|
||||||
|
- canonical CRM routes for `leads`, `chat_logs`, and `kanban`
|
||||||
|
- unified CRM database contract
|
||||||
|
- prompt inventory and persona contract map
|
||||||
|
- full end-to-end async orchestration across all media workflows
|
||||||
|
- stronger frontend/backend contract discipline
|
||||||
|
|
||||||
|
## Deployment Model
|
||||||
|
|
||||||
|
Velocity is being shaped toward:
|
||||||
|
|
||||||
|
- `on-prem primary`
|
||||||
|
- `client-owned cloud optional`
|
||||||
|
- `Linux control surface as operator plane`
|
||||||
|
- `AWS GPU workers as elastic compute`
|
||||||
|
- `S3 as canonical model and large-asset store`
|
||||||
|
|
||||||
|
The current repo also includes the ingress and ops artifacts used to run this model today.
|
||||||
|
|
||||||
|
## Internal Truth Sources
|
||||||
|
|
||||||
|
Key internal docs live under `.Agent Context/` and `infrastructure/`.
|
||||||
|
Important current references include:
|
||||||
|
|
||||||
|
- `Project Velocity Master Bibel`
|
||||||
|
- `Sprint 1 Fact Table`
|
||||||
|
- `TEAM_HANDOFF_2026-04-08.md`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This is an active product repository, not a cleaned SDK.
|
||||||
|
Expect a mix of:
|
||||||
|
|
||||||
|
- production-bound code
|
||||||
|
- live infrastructure artifacts
|
||||||
|
- design explorations
|
||||||
|
- strategic docs
|
||||||
|
- work-in-progress subsystem convergence
|
||||||
|
|
||||||
|
The main direction is stable:
|
||||||
|
Velocity is converging into a private, modular, on-prem real estate operating system with AI, perception, and media generation as first-class components.
|
||||||
|
|||||||
35
app/.codex-readme-shots.mjs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { chromium } from "@playwright/test";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const outDir = path.resolve("../docs/images/readme");
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 });
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
async function shot(name, url, wait = 2000) {
|
||||||
|
await page.goto(url, { waitUntil: "networkidle" });
|
||||||
|
await page.waitForTimeout(wait);
|
||||||
|
await page.screenshot({ path: path.join(outDir, name), fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await shot("login.png", "http://127.0.0.1:5173/login", 1500);
|
||||||
|
|
||||||
|
await page.goto("http://127.0.0.1:5173/login", { waitUntil: "networkidle" });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await page.locator('button').first().click();
|
||||||
|
await page.waitForTimeout(3200);
|
||||||
|
await page.goto("http://127.0.0.1:5173/dashboard", { waitUntil: "networkidle" });
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
await page.screenshot({ path: path.join(outDir, "dashboard.png"), fullPage: true });
|
||||||
|
|
||||||
|
for (const route of ["oracle", "sentinel", "inventory", "catalyst", "settings"]) {
|
||||||
|
await page.goto(`http://127.0.0.1:5173/${route}`, { waitUntil: "networkidle" });
|
||||||
|
await page.waitForTimeout(route === "sentinel" ? 3000 : 2200);
|
||||||
|
await page.screenshot({ path: path.join(outDir, `${route}.png`), fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(outDir);
|
||||||
30
app/dist/index.html
vendored
@@ -1,17 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>Velocity WebOS</title>
|
||||||
<title>Velocity WebOS</title>
|
<script type="module" crossorigin src="./assets/index-CJRJmEe7.js"></script>
|
||||||
<script type="module" crossorigin src="./assets/index-jrtSlzlr.js"></script>
|
<link rel="stylesheet" crossorigin href="./assets/index-UA0RXBVG.css">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BwTUL7MU.css">
|
</head>
|
||||||
</head>
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
</body>
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
202
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
@@ -1,201 +1 @@
|
|||||||
<<<<<<< HEAD
|
{"root":["../../src/app.tsx","../../src/global.d.ts","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/catalystmarketingtab.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/groundtruthpicker.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/modules/sentinel/perceptionplayer.tsx","../../src/components/modules/sentinel/sentinellivesession.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/oracle/mockleads.ts","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/hooks/usecrmbootstrap.ts","../../src/hooks/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/crmmappers.ts","../../src/lib/oraclequeryclient.ts","../../src/lib/utils.ts","../../src/oracle/components/branchbar.tsx","../../src/oracle/components/canvasviewport.tsx","../../src/oracle/components/componentregistry.tsx","../../src/oracle/components/promptrail.tsx","../../src/oracle/components/rollbackconfirmmodal.tsx","../../src/oracle/components/sharemodal.tsx","../../src/oracle/components/renderers/activitystreamrenderer.tsx","../../src/oracle/components/renderers/barchartrenderer.tsx","../../src/oracle/components/renderers/errornoticerenderer.tsx","../../src/oracle/components/renderers/geomaprenderer.tsx","../../src/oracle/components/renderers/kpitilerenderer.tsx","../../src/oracle/components/renderers/linechartrenderer.tsx","../../src/oracle/components/renderers/pipelineboardrenderer.tsx","../../src/oracle/components/renderers/rendererwrapper.tsx","../../src/oracle/components/renderers/tablerenderer.tsx","../../src/oracle/components/renderers/timelinerenderer.tsx","../../src/oracle/components/review/mergereviewdrawer.tsx","../../src/oracle/hooks/useoracleexecution.ts","../../src/oracle/hooks/useoraclepage.ts","../../src/oracle/lib/oracleapiclient.ts","../../src/oracle/lib/oracledemodata.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"}
|
||||||
{
|
|
||||||
"root": [
|
|
||||||
"../../src/app.tsx",
|
|
||||||
"../../src/main.tsx",
|
|
||||||
"../../src/app/oracle/page.tsx",
|
|
||||||
"../../src/components/layout/loginscreen.tsx",
|
|
||||||
"../../src/components/layout/sidebar.tsx",
|
|
||||||
"../../src/components/modules/catalyst.tsx",
|
|
||||||
"../../src/components/modules/dashboard.tsx",
|
|
||||||
"../../src/components/modules/groundtruthpicker.tsx",
|
|
||||||
"../../src/components/modules/inventory.tsx",
|
|
||||||
"../../src/components/modules/oracle.tsx",
|
|
||||||
"../../src/components/modules/sentinel.tsx",
|
|
||||||
"../../src/components/modules/settings.tsx",
|
|
||||||
"../../src/components/oracle/leadinspector.tsx",
|
|
||||||
"../../src/components/oracle/pipelineview.tsx",
|
|
||||||
"../../src/components/oracle/mockleads.ts",
|
|
||||||
"../../src/components/sentinel/journeyriver/inspectorpanel.tsx",
|
|
||||||
"../../src/components/sentinel/journeyriver/riverpath.tsx",
|
|
||||||
"../../src/components/sentinel/journeyriver/index.tsx",
|
|
||||||
"../../src/components/ui/accordion.tsx",
|
|
||||||
"../../src/components/ui/alert-dialog.tsx",
|
|
||||||
"../../src/components/ui/alert.tsx",
|
|
||||||
"../../src/components/ui/aspect-ratio.tsx",
|
|
||||||
"../../src/components/ui/avatar.tsx",
|
|
||||||
"../../src/components/ui/badge.tsx",
|
|
||||||
"../../src/components/ui/breadcrumb.tsx",
|
|
||||||
"../../src/components/ui/button-group.tsx",
|
|
||||||
"../../src/components/ui/button.tsx",
|
|
||||||
"../../src/components/ui/calendar.tsx",
|
|
||||||
"../../src/components/ui/card.tsx",
|
|
||||||
"../../src/components/ui/carousel.tsx",
|
|
||||||
"../../src/components/ui/chart.tsx",
|
|
||||||
"../../src/components/ui/checkbox.tsx",
|
|
||||||
"../../src/components/ui/collapsible.tsx",
|
|
||||||
"../../src/components/ui/command.tsx",
|
|
||||||
"../../src/components/ui/context-menu.tsx",
|
|
||||||
"../../src/components/ui/dialog.tsx",
|
|
||||||
"../../src/components/ui/drawer.tsx",
|
|
||||||
"../../src/components/ui/dropdown-menu.tsx",
|
|
||||||
"../../src/components/ui/empty.tsx",
|
|
||||||
"../../src/components/ui/field.tsx",
|
|
||||||
"../../src/components/ui/form.tsx",
|
|
||||||
"../../src/components/ui/hover-card.tsx",
|
|
||||||
"../../src/components/ui/input-group.tsx",
|
|
||||||
"../../src/components/ui/input-otp.tsx",
|
|
||||||
"../../src/components/ui/input.tsx",
|
|
||||||
"../../src/components/ui/item.tsx",
|
|
||||||
"../../src/components/ui/kbd.tsx",
|
|
||||||
"../../src/components/ui/label.tsx",
|
|
||||||
"../../src/components/ui/menubar.tsx",
|
|
||||||
"../../src/components/ui/navigation-menu.tsx",
|
|
||||||
"../../src/components/ui/pagination.tsx",
|
|
||||||
"../../src/components/ui/popover.tsx",
|
|
||||||
"../../src/components/ui/progress.tsx",
|
|
||||||
"../../src/components/ui/radio-group.tsx",
|
|
||||||
"../../src/components/ui/resizable.tsx",
|
|
||||||
"../../src/components/ui/scroll-area.tsx",
|
|
||||||
"../../src/components/ui/select.tsx",
|
|
||||||
"../../src/components/ui/separator.tsx",
|
|
||||||
"../../src/components/ui/sheet.tsx",
|
|
||||||
"../../src/components/ui/sidebar.tsx",
|
|
||||||
"../../src/components/ui/skeleton.tsx",
|
|
||||||
"../../src/components/ui/slider.tsx",
|
|
||||||
"../../src/components/ui/sonner.tsx",
|
|
||||||
"../../src/components/ui/spinner.tsx",
|
|
||||||
"../../src/components/ui/switch.tsx",
|
|
||||||
"../../src/components/ui/table.tsx",
|
|
||||||
"../../src/components/ui/tabs.tsx",
|
|
||||||
"../../src/components/ui/textarea.tsx",
|
|
||||||
"../../src/components/ui/toggle-group.tsx",
|
|
||||||
"../../src/components/ui/toggle.tsx",
|
|
||||||
"../../src/components/ui/tooltip.tsx",
|
|
||||||
"../../src/hooks/use-mobile.ts",
|
|
||||||
"../../src/lib/oraclequeryclient.ts",
|
|
||||||
"../../src/lib/utils.ts",
|
|
||||||
"../../src/oracle/components/branchbar.tsx",
|
|
||||||
"../../src/oracle/components/canvasviewport.tsx",
|
|
||||||
"../../src/oracle/components/componentregistry.tsx",
|
|
||||||
"../../src/oracle/components/promptrail.tsx",
|
|
||||||
"../../src/oracle/components/rollbackconfirmmodal.tsx",
|
|
||||||
"../../src/oracle/components/sharemodal.tsx",
|
|
||||||
"../../src/oracle/components/renderers/activitystreamrenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/barchartrenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/errornoticerenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/geomaprenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/kpitilerenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/linechartrenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/pipelineboardrenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/rendererwrapper.tsx",
|
|
||||||
"../../src/oracle/components/renderers/tablerenderer.tsx",
|
|
||||||
"../../src/oracle/components/renderers/timelinerenderer.tsx",
|
|
||||||
"../../src/oracle/components/review/mergereviewdrawer.tsx",
|
|
||||||
"../../src/oracle/hooks/useoracleexecution.ts",
|
|
||||||
"../../src/oracle/hooks/useoraclepage.ts",
|
|
||||||
"../../src/oracle/lib/oracleapiclient.ts",
|
|
||||||
"../../src/oracle/lib/oracledemodata.ts",
|
|
||||||
"../../src/oracle/types/canvas.ts",
|
|
||||||
"../../src/store/usecurrencystore.ts",
|
|
||||||
"../../src/store/usemarketingstore.ts",
|
|
||||||
"../../src/store/usestore.ts",
|
|
||||||
"../../src/types/crm.ts",
|
|
||||||
"../../src/types/index.ts",
|
|
||||||
"../../src/utils/curvegenerator.ts"
|
|
||||||
],
|
|
||||||
"version": "5.9.3"
|
|
||||||
}
|
|
||||||
=======
|
|
||||||
{
|
|
||||||
"root": [
|
|
||||||
"../../src/app.tsx",
|
|
||||||
"../../src/global.d.ts",
|
|
||||||
"../../src/main.tsx",
|
|
||||||
"../../src/app/oracle/page.tsx",
|
|
||||||
"../../src/components/layout/loginscreen.tsx",
|
|
||||||
"../../src/components/layout/notificationcenter.tsx",
|
|
||||||
"../../src/components/layout/sidebar.tsx",
|
|
||||||
"../../src/components/modules/catalyst.tsx",
|
|
||||||
"../../src/components/modules/dashboard.tsx",
|
|
||||||
"../../src/components/modules/inventory.tsx",
|
|
||||||
"../../src/components/modules/oracle.tsx",
|
|
||||||
"../../src/components/modules/sentinel.tsx",
|
|
||||||
"../../src/components/modules/settings.tsx",
|
|
||||||
"../../src/components/modules/sentinel/perceptionplayer.tsx",
|
|
||||||
"../../src/components/modules/sentinel/sentinellivesession.tsx",
|
|
||||||
"../../src/components/oracle/leadinspector.tsx",
|
|
||||||
"../../src/components/oracle/pipelineview.tsx",
|
|
||||||
"../../src/components/oracle/mockleads.ts",
|
|
||||||
"../../src/components/sentinel/journeyriver/inspectorpanel.tsx",
|
|
||||||
"../../src/components/sentinel/journeyriver/riverpath.tsx",
|
|
||||||
"../../src/components/sentinel/journeyriver/index.tsx",
|
|
||||||
"../../src/components/ui/accordion.tsx",
|
|
||||||
"../../src/components/ui/alert-dialog.tsx",
|
|
||||||
"../../src/components/ui/alert.tsx",
|
|
||||||
"../../src/components/ui/aspect-ratio.tsx",
|
|
||||||
"../../src/components/ui/avatar.tsx",
|
|
||||||
"../../src/components/ui/badge.tsx",
|
|
||||||
"../../src/components/ui/breadcrumb.tsx",
|
|
||||||
"../../src/components/ui/button-group.tsx",
|
|
||||||
"../../src/components/ui/button.tsx",
|
|
||||||
"../../src/components/ui/calendar.tsx",
|
|
||||||
"../../src/components/ui/card.tsx",
|
|
||||||
"../../src/components/ui/carousel.tsx",
|
|
||||||
"../../src/components/ui/chart.tsx",
|
|
||||||
"../../src/components/ui/checkbox.tsx",
|
|
||||||
"../../src/components/ui/collapsible.tsx",
|
|
||||||
"../../src/components/ui/command.tsx",
|
|
||||||
"../../src/components/ui/context-menu.tsx",
|
|
||||||
"../../src/components/ui/dialog.tsx",
|
|
||||||
"../../src/components/ui/drawer.tsx",
|
|
||||||
"../../src/components/ui/dropdown-menu.tsx",
|
|
||||||
"../../src/components/ui/empty.tsx",
|
|
||||||
"../../src/components/ui/field.tsx",
|
|
||||||
"../../src/components/ui/form.tsx",
|
|
||||||
"../../src/components/ui/hover-card.tsx",
|
|
||||||
"../../src/components/ui/input-group.tsx",
|
|
||||||
"../../src/components/ui/input-otp.tsx",
|
|
||||||
"../../src/components/ui/input.tsx",
|
|
||||||
"../../src/components/ui/item.tsx",
|
|
||||||
"../../src/components/ui/kbd.tsx",
|
|
||||||
"../../src/components/ui/label.tsx",
|
|
||||||
"../../src/components/ui/menubar.tsx",
|
|
||||||
"../../src/components/ui/navigation-menu.tsx",
|
|
||||||
"../../src/components/ui/pagination.tsx",
|
|
||||||
"../../src/components/ui/popover.tsx",
|
|
||||||
"../../src/components/ui/progress.tsx",
|
|
||||||
"../../src/components/ui/radio-group.tsx",
|
|
||||||
"../../src/components/ui/resizable.tsx",
|
|
||||||
"../../src/components/ui/scroll-area.tsx",
|
|
||||||
"../../src/components/ui/select.tsx",
|
|
||||||
"../../src/components/ui/separator.tsx",
|
|
||||||
"../../src/components/ui/sheet.tsx",
|
|
||||||
"../../src/components/ui/sidebar.tsx",
|
|
||||||
"../../src/components/ui/skeleton.tsx",
|
|
||||||
"../../src/components/ui/slider.tsx",
|
|
||||||
"../../src/components/ui/sonner.tsx",
|
|
||||||
"../../src/components/ui/spinner.tsx",
|
|
||||||
"../../src/components/ui/switch.tsx",
|
|
||||||
"../../src/components/ui/table.tsx",
|
|
||||||
"../../src/components/ui/tabs.tsx",
|
|
||||||
"../../src/components/ui/textarea.tsx",
|
|
||||||
"../../src/components/ui/toggle-group.tsx",
|
|
||||||
"../../src/components/ui/toggle.tsx",
|
|
||||||
"../../src/components/ui/tooltip.tsx",
|
|
||||||
"../../src/hooks/use-mobile.ts",
|
|
||||||
"../../src/hooks/usemediapipefacelandmarker.ts",
|
|
||||||
"../../src/hooks/usevelocitysocket.ts",
|
|
||||||
"../../src/lib/api.ts",
|
|
||||||
"../../src/lib/oraclequeryclient.ts",
|
|
||||||
"../../src/lib/utils.ts",
|
|
||||||
"../../src/store/usemarketingstore.ts",
|
|
||||||
"../../src/store/usestore.ts",
|
|
||||||
"../../src/types/crm.ts",
|
|
||||||
"../../src/types/index.ts",
|
|
||||||
"../../src/utils/curvegenerator.ts",
|
|
||||||
"../../src/utils/landmarkpacketencoder.ts"
|
|
||||||
],
|
|
||||||
"version": "5.9.3"
|
|
||||||
}
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
18
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
@@ -1,26 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
<<<<<<< HEAD
|
|
||||||
createSlot
|
|
||||||
} from "./chunk-YWBEB5PG.js";
|
|
||||||
import {
|
|
||||||
require_shim
|
|
||||||
} from "./chunk-TXHHHGR3.js";
|
|
||||||
import {
|
|
||||||
=======
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
useCallbackRef,
|
useCallbackRef,
|
||||||
useLayoutEffect2
|
useLayoutEffect2
|
||||||
} from "./chunk-GRXJTWBV.js";
|
} from "./chunk-GRXJTWBV.js";
|
||||||
import {
|
import {
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
require_shim
|
|
||||||
} from "./chunk-642Z5WD3.js";
|
|
||||||
import {
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
require_react_dom
|
require_react_dom
|
||||||
} from "./chunk-YLZ34CCM.js";
|
} from "./chunk-YLZ34CCM.js";
|
||||||
|
import {
|
||||||
|
require_shim
|
||||||
|
} from "./chunk-642Z5WD3.js";
|
||||||
import {
|
import {
|
||||||
createSlot
|
createSlot
|
||||||
} from "./chunk-5HUACAZ7.js";
|
} from "./chunk-5HUACAZ7.js";
|
||||||
|
|||||||
3706
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
14
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
@@ -28,23 +28,13 @@ import {
|
|||||||
useLoader,
|
useLoader,
|
||||||
useStore,
|
useStore,
|
||||||
useThree
|
useThree
|
||||||
<<<<<<< HEAD
|
} from "./chunk-JRJA23OI.js";
|
||||||
} from "./chunk-5ESDTKMP.js";
|
|
||||||
import "./chunk-NJ4V5H3P.js";
|
|
||||||
import "./chunk-L3Z576C2.js";
|
|
||||||
import "./chunk-GUQHL3N7.js";
|
|
||||||
import "./chunk-TXHHHGR3.js";
|
|
||||||
import "./chunk-2YVA4HRZ.js";
|
|
||||||
import "./chunk-WUR7D6NS.js";
|
|
||||||
=======
|
|
||||||
} from "./chunk-CSHY5MMV.js";
|
|
||||||
import "./chunk-LTNRPUSL.js";
|
|
||||||
import "./chunk-INS7YHTD.js";
|
import "./chunk-INS7YHTD.js";
|
||||||
import "./chunk-QURGMCZB.js";
|
import "./chunk-QURGMCZB.js";
|
||||||
|
import "./chunk-LTNRPUSL.js";
|
||||||
import "./chunk-642Z5WD3.js";
|
import "./chunk-642Z5WD3.js";
|
||||||
import "./chunk-USXRE7Q2.js";
|
import "./chunk-USXRE7Q2.js";
|
||||||
import "./chunk-ZNKPWGXJ.js";
|
import "./chunk-ZNKPWGXJ.js";
|
||||||
>>>>>>> feat/#15
|
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
export {
|
export {
|
||||||
Canvas,
|
Canvas,
|
||||||
|
|||||||
491
app/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,343 +1,196 @@
|
|||||||
{
|
{
|
||||||
<<<<<<< HEAD
|
|
||||||
"hash": "b2c5007d",
|
|
||||||
"configHash": "d9a82a01",
|
|
||||||
"lockfileHash": "8a04eea8",
|
|
||||||
"browserHash": "c208d4ff",
|
|
||||||
=======
|
|
||||||
"hash": "48124858",
|
"hash": "48124858",
|
||||||
"configHash": "2be684c6",
|
"configHash": "2be684c6",
|
||||||
"lockfileHash": "dbdb05fd",
|
"lockfileHash": "dbdb05fd",
|
||||||
"browserHash": "4b08622a",
|
"browserHash": "bc295ff7",
|
||||||
>>>>>>> feat/#15
|
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"react": {
|
"react": {
|
||||||
"src": "../../react/index.js",
|
"src": "../../react/index.js",
|
||||||
"file": "react.js",
|
"file": "react.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "ca909492",
|
||||||
"fileHash": "a26efb1d",
|
|
||||||
=======
|
|
||||||
"fileHash": "54ab4c2f",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"src": "../../react-dom/index.js",
|
"src": "../../react-dom/index.js",
|
||||||
"file": "react-dom.js",
|
"file": "react-dom.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "9ea60d36",
|
||||||
"fileHash": "c6e68a6f",
|
|
||||||
=======
|
|
||||||
"fileHash": "ae1b5cb6",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-dev-runtime": {
|
"react/jsx-dev-runtime": {
|
||||||
"src": "../../react/jsx-dev-runtime.js",
|
"src": "../../react/jsx-dev-runtime.js",
|
||||||
"file": "react_jsx-dev-runtime.js",
|
"file": "react_jsx-dev-runtime.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "f778ce34",
|
||||||
"fileHash": "7531df54",
|
|
||||||
=======
|
|
||||||
"fileHash": "835e3f15",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-runtime": {
|
"react/jsx-runtime": {
|
||||||
"src": "../../react/jsx-runtime.js",
|
"src": "../../react/jsx-runtime.js",
|
||||||
"file": "react_jsx-runtime.js",
|
"file": "react_jsx-runtime.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "afe32f9c",
|
||||||
"fileHash": "a247453f",
|
|
||||||
=======
|
|
||||||
"fileHash": "4c9e32b0",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"@radix-ui/react-avatar": {
|
"@radix-ui/react-avatar": {
|
||||||
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
|
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
|
||||||
"file": "@radix-ui_react-avatar.js",
|
"file": "@radix-ui_react-avatar.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "78604ab7",
|
||||||
"fileHash": "d86de0d6",
|
|
||||||
=======
|
|
||||||
"fileHash": "953014a7",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@radix-ui/react-dropdown-menu": {
|
"@radix-ui/react-dropdown-menu": {
|
||||||
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
|
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
|
||||||
"file": "@radix-ui_react-dropdown-menu.js",
|
"file": "@radix-ui_react-dropdown-menu.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "7e6567c2",
|
||||||
"fileHash": "51cbea4a",
|
|
||||||
=======
|
|
||||||
"fileHash": "793919d7",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@radix-ui/react-slot": {
|
"@radix-ui/react-slot": {
|
||||||
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
||||||
"file": "@radix-ui_react-slot.js",
|
"file": "@radix-ui_react-slot.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "4f153a2d",
|
||||||
"fileHash": "d49e4181",
|
|
||||||
=======
|
|
||||||
"fileHash": "4ee317ec",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@react-three/drei": {
|
"@react-three/drei": {
|
||||||
"src": "../../@react-three/drei/index.js",
|
"src": "../../@react-three/drei/index.js",
|
||||||
"file": "@react-three_drei.js",
|
"file": "@react-three_drei.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "313a1f02",
|
||||||
"fileHash": "49af0e0c",
|
|
||||||
=======
|
|
||||||
"fileHash": "bb1a1525",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@react-three/fiber": {
|
"@react-three/fiber": {
|
||||||
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
||||||
"file": "@react-three_fiber.js",
|
"file": "@react-three_fiber.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "5e5643b4",
|
||||||
"fileHash": "6e6f65b1",
|
|
||||||
=======
|
|
||||||
"fileHash": "7a3ce954",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"class-variance-authority": {
|
"class-variance-authority": {
|
||||||
"src": "../../class-variance-authority/dist/index.mjs",
|
"src": "../../class-variance-authority/dist/index.mjs",
|
||||||
"file": "class-variance-authority.js",
|
"file": "class-variance-authority.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "69d37784",
|
||||||
"fileHash": "0e514934",
|
|
||||||
=======
|
|
||||||
"fileHash": "80d8c035",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"clsx": {
|
"clsx": {
|
||||||
"src": "../../clsx/dist/clsx.mjs",
|
"src": "../../clsx/dist/clsx.mjs",
|
||||||
"file": "clsx.js",
|
"file": "clsx.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "861ef14c",
|
||||||
"fileHash": "e71ef32c",
|
|
||||||
=======
|
|
||||||
"fileHash": "bb4a0943",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"framer-motion": {
|
"framer-motion": {
|
||||||
"src": "../../framer-motion/dist/es/index.mjs",
|
"src": "../../framer-motion/dist/es/index.mjs",
|
||||||
"file": "framer-motion.js",
|
"file": "framer-motion.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "3f8d4bda",
|
||||||
"fileHash": "5b5818ee",
|
|
||||||
=======
|
|
||||||
"fileHash": "9729455b",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"lucide-react": {
|
"lucide-react": {
|
||||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||||
"file": "lucide-react.js",
|
"file": "lucide-react.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "f8a0e731",
|
||||||
"fileHash": "fb6a8921",
|
|
||||||
=======
|
|
||||||
"fileHash": "cdd57b8d",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-dom/client": {
|
"react-dom/client": {
|
||||||
"src": "../../react-dom/client.js",
|
"src": "../../react-dom/client.js",
|
||||||
"file": "react-dom_client.js",
|
"file": "react-dom_client.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "bb1fb188",
|
||||||
"fileHash": "032f0a73",
|
|
||||||
=======
|
|
||||||
"fileHash": "49cc8986",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router-dom": {
|
||||||
"src": "../../react-router-dom/dist/index.mjs",
|
"src": "../../react-router-dom/dist/index.mjs",
|
||||||
"file": "react-router-dom.js",
|
"file": "react-router-dom.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "69a5af47",
|
||||||
"fileHash": "fa45c285",
|
|
||||||
=======
|
|
||||||
"fileHash": "9982d9b7",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"recharts": {
|
"recharts": {
|
||||||
"src": "../../recharts/es6/index.js",
|
"src": "../../recharts/es6/index.js",
|
||||||
"file": "recharts.js",
|
"file": "recharts.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "8c3719f7",
|
||||||
"fileHash": "b3d6765a",
|
|
||||||
=======
|
|
||||||
"fileHash": "a27d1b2d",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"tailwind-merge": {
|
"sonner": {
|
||||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
"src": "../../sonner/dist/index.mjs",
|
||||||
"file": "tailwind-merge.js",
|
"file": "sonner.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "ff7fef4b",
|
||||||
"fileHash": "47183e49",
|
|
||||||
=======
|
|
||||||
"fileHash": "3b07b318",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"three": {
|
"tailwind-merge": {
|
||||||
"src": "../../three/build/three.module.js",
|
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||||
"file": "three.js",
|
"file": "tailwind-merge.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "b1f20ce9",
|
||||||
"fileHash": "d78c89ed",
|
|
||||||
=======
|
|
||||||
"fileHash": "a18f1fc6",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"zustand": {
|
"three": {
|
||||||
"src": "../../zustand/esm/index.mjs",
|
"src": "../../three/build/three.module.js",
|
||||||
"file": "zustand.js",
|
"file": "three.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "cae86099",
|
||||||
"fileHash": "39f09270",
|
|
||||||
=======
|
|
||||||
"fileHash": "179748be",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"zustand/middleware": {
|
"zustand": {
|
||||||
"src": "../../zustand/esm/middleware.mjs",
|
"src": "../../zustand/esm/index.mjs",
|
||||||
"file": "zustand_middleware.js",
|
"file": "zustand.js",
|
||||||
<<<<<<< HEAD
|
"fileHash": "60f9f3ea",
|
||||||
"fileHash": "ce09abfc",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"sonner": {
|
|
||||||
"src": "../../sonner/dist/index.mjs",
|
|
||||||
"file": "sonner.js",
|
|
||||||
"fileHash": "b1e28aee",
|
|
||||||
=======
|
|
||||||
"fileHash": "89cbbc77",
|
|
||||||
>>>>>>> feat/#15
|
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
},
|
||||||
},
|
"zustand/middleware": {
|
||||||
"chunks": {
|
"src": "../../zustand/esm/middleware.mjs",
|
||||||
"hls-Q6LDPZPT": {
|
"file": "zustand_middleware.js",
|
||||||
"file": "hls-Q6LDPZPT.js"
|
"fileHash": "3b17a615",
|
||||||
},
|
"needsInterop": false
|
||||||
"vision_bundle-ZAS5UOAV": {
|
}
|
||||||
"file": "vision_bundle-ZAS5UOAV.js"
|
},
|
||||||
},
|
"chunks": {
|
||||||
"chunk-QJTQF54Q": {
|
"hls-Q6LDPZPT": {
|
||||||
"file": "chunk-QJTQF54Q.js"
|
"file": "hls-Q6LDPZPT.js"
|
||||||
},
|
},
|
||||||
<<<<<<< HEAD
|
"vision_bundle-ZAS5UOAV": {
|
||||||
"chunk-O4L7C4YS": {
|
"file": "vision_bundle-ZAS5UOAV.js"
|
||||||
"file": "chunk-O4L7C4YS.js"
|
},
|
||||||
},
|
"chunk-H4GSM2WL": {
|
||||||
"chunk-7GZ4CI6Q": {
|
"file": "chunk-H4GSM2WL.js"
|
||||||
"file": "chunk-7GZ4CI6Q.js"
|
},
|
||||||
},
|
|
||||||
"chunk-7GZ4CI6Q": {
|
|
||||||
"file": "chunk-7GZ4CI6Q.js"
|
|
||||||
},
|
|
||||||
"chunk-OAEA5FZL": {
|
|
||||||
"file": "chunk-OAEA5FZL.js"
|
|
||||||
},
|
|
||||||
"chunk-5ESDTKMP": {
|
|
||||||
"file": "chunk-5ESDTKMP.js""chunk-5ESDTKMP": {
|
|
||||||
"file": "chunk-5ESDTKMP.js"
|
|
||||||
},
|
|
||||||
"chunk-NJ4V5H3P": {
|
|
||||||
"file": "chunk-NJ4V5H3P.js""chunk-NJ4V5H3P": {
|
|
||||||
"file": "chunk-NJ4V5H3P.js"
|
|
||||||
},
|
|
||||||
"chunk-L3Z576C2": {
|
|
||||||
"file": "chunk-L3Z576C2.js"
|
|
||||||
},
|
|
||||||
"chunk-6MXH2QM6": {
|
|
||||||
"file": "chunk-6MXH2QM6.js""chunk-6MXH2QM6": {
|
|
||||||
"file": "chunk-6MXH2QM6.js"
|
|
||||||
},
|
|
||||||
"chunk-GUQHL3N7": {
|
|
||||||
"file": "chunk-GUQHL3N7.js""chunk-GUQHL3N7": {
|
|
||||||
"file": "chunk-GUQHL3N7.js"
|
|
||||||
},
|
|
||||||
"chunk-EQCCHGRT": {
|
|
||||||
"file": "chunk-EQCCHGRT.js"
|
|
||||||
},
|
|
||||||
"chunk-YWBEB5PG": {
|
|
||||||
"file": "chunk-YWBEB5PG.js"
|
|
||||||
},
|
|
||||||
"chunk-YWBEB5PG": {
|
|
||||||
"file": "chunk-YWBEB5PG.js"
|
|
||||||
},
|
|
||||||
"chunk-TXHHHGR3": {
|
|
||||||
"file": "chunk-TXHHHGR3.js"
|
|
||||||
},
|
|
||||||
"chunk-23FVUG5N": {
|
|
||||||
"file": "chunk-23FVUG5N.js"
|
|
||||||
},
|
|
||||||
"chunk-2VUH7NEY": {
|
|
||||||
"file": "chunk-2VUH7NEY.js"
|
|
||||||
},
|
|
||||||
"chunk-23FVUG5N": {
|
|
||||||
"file": "chunk-23FVUG5N.js"
|
|
||||||
},
|
|
||||||
"chunk-2VUH7NEY": {
|
|
||||||
"file": "chunk-2VUH7NEY.js"
|
|
||||||
},
|
|
||||||
"chunk-YF4B4G2L": {
|
|
||||||
"file": "chunk-YF4B4G2L.js"
|
|
||||||
=======
|
|
||||||
"chunk-XGWIEMTH": {
|
"chunk-XGWIEMTH": {
|
||||||
"file": "chunk-XGWIEMTH.js"
|
"file": "chunk-XGWIEMTH.js"
|
||||||
},
|
},
|
||||||
"chunk-H4GSM2WL": {
|
"chunk-OAEA5FZL": {
|
||||||
"file": "chunk-H4GSM2WL.js"
|
"file": "chunk-OAEA5FZL.js"
|
||||||
},
|
},
|
||||||
"chunk-OAEA5FZL": {
|
"chunk-AFNBKP7P": {
|
||||||
"file": "chunk-OAEA5FZL.js"
|
"file": "chunk-AFNBKP7P.js"
|
||||||
},
|
},
|
||||||
"chunk-CSHY5MMV": {
|
"chunk-QJTQF54Q": {
|
||||||
"file": "chunk-CSHY5MMV.js"
|
"file": "chunk-QJTQF54Q.js"
|
||||||
},
|
},
|
||||||
"chunk-LTNRPUSL": {
|
"chunk-JRJA23OI": {
|
||||||
"file": "chunk-LTNRPUSL.js"
|
"file": "chunk-JRJA23OI.js"
|
||||||
},
|
},
|
||||||
"chunk-INS7YHTD": {
|
"chunk-INS7YHTD": {
|
||||||
"file": "chunk-INS7YHTD.js"
|
"file": "chunk-INS7YHTD.js"
|
||||||
},
|
},
|
||||||
"chunk-AFNBKP7P": {
|
"chunk-QURGMCZB": {
|
||||||
"file": "chunk-AFNBKP7P.js"
|
"file": "chunk-QURGMCZB.js"
|
||||||
},
|
},
|
||||||
"chunk-QURGMCZB": {
|
"chunk-LTNRPUSL": {
|
||||||
"file": "chunk-QURGMCZB.js"
|
"file": "chunk-LTNRPUSL.js"
|
||||||
},
|
},
|
||||||
"chunk-GRXJTWBV": {
|
"chunk-U7P2NEEE": {
|
||||||
"file": "chunk-GRXJTWBV.js"
|
"file": "chunk-U7P2NEEE.js"
|
||||||
},
|
},
|
||||||
"chunk-642Z5WD3": {
|
"chunk-GRXJTWBV": {
|
||||||
"file": "chunk-642Z5WD3.js"
|
"file": "chunk-GRXJTWBV.js"
|
||||||
},
|
},
|
||||||
"chunk-YLZ34CCM": {
|
"chunk-YLZ34CCM": {
|
||||||
"file": "chunk-YLZ34CCM.js"
|
"file": "chunk-YLZ34CCM.js"
|
||||||
>>>>>>> feat/#15
|
},
|
||||||
},
|
"chunk-642Z5WD3": {
|
||||||
"chunk-5HUACAZ7": {
|
"file": "chunk-642Z5WD3.js"
|
||||||
"file": "chunk-5HUACAZ7.js"
|
},
|
||||||
},
|
"chunk-5HUACAZ7": {
|
||||||
"chunk-HPBHRBIF": {
|
"file": "chunk-5HUACAZ7.js"
|
||||||
"file": "chunk-HPBHRBIF.js"
|
},
|
||||||
},
|
"chunk-HPBHRBIF": {
|
||||||
"chunk-USXRE7Q2": {
|
"file": "chunk-HPBHRBIF.js"
|
||||||
"file": "chunk-USXRE7Q2.js"
|
},
|
||||||
},
|
"chunk-USXRE7Q2": {
|
||||||
"chunk-ZNKPWGXJ": {
|
"file": "chunk-USXRE7Q2.js"
|
||||||
"file": "chunk-ZNKPWGXJ.js"
|
},
|
||||||
},
|
"chunk-ZNKPWGXJ": {
|
||||||
"chunk-U7P2NEEE": {
|
"file": "chunk-ZNKPWGXJ.js"
|
||||||
"file": "chunk-U7P2NEEE.js"
|
},
|
||||||
},
|
"chunk-G3PMV62Z": {
|
||||||
"chunk-G3PMV62Z": {
|
"file": "chunk-G3PMV62Z.js"
|
||||||
"file": "chunk-G3PMV62Z.js"
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
6
app/node_modules/.vite/deps/recharts.js
generated
vendored
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
_extends
|
_extends
|
||||||
} from "./chunk-H4GSM2WL.js";
|
} from "./chunk-H4GSM2WL.js";
|
||||||
|
import {
|
||||||
|
clsx_default
|
||||||
|
} from "./chunk-U7P2NEEE.js";
|
||||||
import {
|
import {
|
||||||
require_react_dom
|
require_react_dom
|
||||||
} from "./chunk-YLZ34CCM.js";
|
} from "./chunk-YLZ34CCM.js";
|
||||||
import {
|
import {
|
||||||
require_react
|
require_react
|
||||||
} from "./chunk-ZNKPWGXJ.js";
|
} from "./chunk-ZNKPWGXJ.js";
|
||||||
import {
|
|
||||||
clsx_default
|
|
||||||
} from "./chunk-U7P2NEEE.js";
|
|
||||||
import {
|
import {
|
||||||
__commonJS,
|
__commonJS,
|
||||||
__export,
|
__export,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Inventory } from '@/components/modules/Inventory';
|
|||||||
import { Settings } from '@/components/modules/Settings';
|
import { Settings } from '@/components/modules/Settings';
|
||||||
import { Catalyst } from '@/components/modules/Catalyst';
|
import { Catalyst } from '@/components/modules/Catalyst';
|
||||||
import { NotificationCenter } from '@/components/layout/NotificationCenter';
|
import { NotificationCenter } from '@/components/layout/NotificationCenter';
|
||||||
|
import { useCrmBootstrap } from '@/hooks/useCrmBootstrap';
|
||||||
import type { ModuleId } from '@/types';
|
import type { ModuleId } from '@/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +76,7 @@ function RouteModuleSync() {
|
|||||||
|
|
||||||
function MainLayout() {
|
function MainLayout() {
|
||||||
const { activeModule, setActiveModule, sidebarExpanded, logout } = useStore();
|
const { activeModule, setActiveModule, sidebarExpanded, logout } = useStore();
|
||||||
|
useCrmBootstrap();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useMarketingStore } from '@/store/useMarketingStore';
|
|||||||
import { useCurrency } from '@/store/useCurrencyStore';
|
import { useCurrency } from '@/store/useCurrencyStore';
|
||||||
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
|
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
|
||||||
import { GroundTruthPicker } from './GroundTruthPicker';
|
import { GroundTruthPicker } from './GroundTruthPicker';
|
||||||
|
import { CatalystMarketingTab } from './CatalystMarketingTab';
|
||||||
import type { GroundTruthSelection } from './GroundTruthPicker';
|
import type { GroundTruthSelection } from './GroundTruthPicker';
|
||||||
|
|
||||||
// ── Design tokens ─────────────────────────────────────────────────────────────
|
// ── Design tokens ─────────────────────────────────────────────────────────────
|
||||||
@@ -936,13 +937,14 @@ function WarRoom() {
|
|||||||
// Tab Bar
|
// Tab Bar
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room';
|
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
|
||||||
|
|
||||||
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
||||||
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
|
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
|
||||||
{ id: 'command', label: 'Campaign Command', icon: Megaphone },
|
{ id: 'command', label: 'Campaign Command', icon: Megaphone },
|
||||||
{ id: 'intelligence', label: 'Intelligence & ROI', icon: BarChart3 },
|
{ id: 'intelligence', label: 'Intelligence & ROI', icon: BarChart3 },
|
||||||
{ id: 'war-room', label: 'War Room', icon: Globe },
|
{ id: 'war-room', label: 'War Room', icon: Globe },
|
||||||
|
{ id: 'marketing', label: 'Marketing', icon: TrendingUp },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -957,6 +959,7 @@ export function Catalyst() {
|
|||||||
'command': <CampaignCommand />,
|
'command': <CampaignCommand />,
|
||||||
'intelligence': <IntelligenceROI />,
|
'intelligence': <IntelligenceROI />,
|
||||||
'war-room': <WarRoom />,
|
'war-room': <WarRoom />,
|
||||||
|
'marketing': <CatalystMarketingTab />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
263
app/src/components/modules/CatalystMarketingTab.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Activity, BarChart3, DatabaseZap, Megaphone, RefreshCw, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCatalystCampaigns,
|
||||||
|
getLeadDemographics,
|
||||||
|
getSentimentScatter,
|
||||||
|
seedSyntheticLeads,
|
||||||
|
type LeadDemographics,
|
||||||
|
type MarketingCampaignSummary,
|
||||||
|
type ScatterDataPoint,
|
||||||
|
} from '@/lib/api';
|
||||||
|
|
||||||
|
function formatMoney(value: number) {
|
||||||
|
return new Intl.NumberFormat('en-AE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'AED',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionCard({
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
children,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon: typeof Activity;
|
||||||
|
subtitle?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="rounded-2xl border border-white/10 bg-[rgba(8,10,18,0.82)] p-5">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-xl border border-blue-400/20 bg-blue-500/10">
|
||||||
|
<Icon className="h-4 w-4 text-blue-300" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-white">{title}</h3>
|
||||||
|
{subtitle && <p className="text-xs text-white/50 mt-0.5">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryMetric({ label, value }: { label: string; value: string | number }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4">
|
||||||
|
<div className="text-[11px] uppercase tracking-[0.18em] text-white/45">{label}</div>
|
||||||
|
<div className="mt-2 text-2xl font-semibold text-white">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CatalystMarketingTab() {
|
||||||
|
const [campaigns, setCampaigns] = useState<MarketingCampaignSummary[]>([]);
|
||||||
|
const [scatter, setScatter] = useState<ScatterDataPoint[]>([]);
|
||||||
|
const [demographics, setDemographics] = useState<LeadDemographics | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [seeding, setSeeding] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const [campaignRows, scatterRows, demographicRows] = await Promise.all([
|
||||||
|
getCatalystCampaigns(),
|
||||||
|
getSentimentScatter(),
|
||||||
|
getLeadDemographics(),
|
||||||
|
]);
|
||||||
|
if (!active) return;
|
||||||
|
setCampaigns(campaignRows);
|
||||||
|
setScatter(scatterRows);
|
||||||
|
setDemographics(demographicRows);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
if (!active) return;
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load marketing intelligence');
|
||||||
|
} finally {
|
||||||
|
if (active) setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void load();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const totals = useMemo(() => {
|
||||||
|
const totalBudget = campaigns.reduce((sum, campaign) => sum + campaign.budget, 0);
|
||||||
|
const totalSpent = campaigns.reduce((sum, campaign) => sum + campaign.spent, 0);
|
||||||
|
const totalLeads = scatter.length;
|
||||||
|
const whales = scatter.filter((item) => item.qualification === 'WHALE').length;
|
||||||
|
const avgSentiment = totalLeads
|
||||||
|
? Math.round(scatter.reduce((sum, item) => sum + item.sentiment_score, 0) / totalLeads)
|
||||||
|
: 0;
|
||||||
|
return { totalBudget, totalSpent, totalLeads, whales, avgSentiment };
|
||||||
|
}, [campaigns, scatter]);
|
||||||
|
|
||||||
|
const handleSeed = async () => {
|
||||||
|
setSeeding(true);
|
||||||
|
try {
|
||||||
|
await seedSyntheticLeads(100);
|
||||||
|
const [scatterRows, demographicRows] = await Promise.all([
|
||||||
|
getSentimentScatter(),
|
||||||
|
getLeadDemographics(),
|
||||||
|
]);
|
||||||
|
setScatter(scatterRows);
|
||||||
|
setDemographics(demographicRows);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Synthetic seed failed');
|
||||||
|
} finally {
|
||||||
|
setSeeding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SectionCard
|
||||||
|
title="Integrated Marketing Surface"
|
||||||
|
subtitle="Sourik-style campaign intelligence stacked inside the root Catalyst shell."
|
||||||
|
icon={Sparkles}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-5">
|
||||||
|
<SummaryMetric label="Campaigns" value={campaigns.length} />
|
||||||
|
<SummaryMetric label="Tracked Leads" value={totals.totalLeads} />
|
||||||
|
<SummaryMetric label="Whales" value={totals.whales} />
|
||||||
|
<SummaryMetric label="Avg Sentiment" value={totals.avgSentiment} />
|
||||||
|
<SummaryMetric label="Active Spend" value={formatMoney(totals.totalSpent)} />
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Campaign Manager"
|
||||||
|
subtitle="Unified Meta-first campaign strip, vertically ported into the Marketing sub-tab."
|
||||||
|
icon={Megaphone}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-white/50">Loading campaign intelligence…</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{campaigns.map((campaign) => (
|
||||||
|
<div key={campaign.id} className="rounded-xl border border-white/8 bg-white/5 p-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-white">{campaign.name}</div>
|
||||||
|
<div className="mt-1 text-[11px] uppercase tracking-wide text-white/45">
|
||||||
|
{campaign.platform} · {campaign.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-xs text-white/65">
|
||||||
|
<div>Budget {formatMoney(campaign.budget)}</div>
|
||||||
|
<div>Spent {formatMoney(campaign.spent)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-3 text-xs text-white/65 md:grid-cols-4">
|
||||||
|
<div>Impressions {campaign.impressions.toLocaleString()}</div>
|
||||||
|
<div>Clicks {campaign.clicks.toLocaleString()}</div>
|
||||||
|
<div>Conversions {campaign.conversions.toLocaleString()}</div>
|
||||||
|
<div>
|
||||||
|
CTR {campaign.impressions ? ((campaign.clicks / campaign.impressions) * 100).toFixed(2) : '0.00'}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Lead Intelligence Feed"
|
||||||
|
subtitle="Live analytics from the root CRM routes."
|
||||||
|
icon={BarChart3}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[1.35fr_0.65fr]">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{scatter.slice(0, 14).map((lead) => (
|
||||||
|
<div key={lead.id} className="rounded-xl border border-white/8 bg-white/5 p-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-white">{lead.name}</div>
|
||||||
|
<div className="text-xs text-white/50">
|
||||||
|
{lead.qualification} · {lead.kanban_status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-xs text-white/65">
|
||||||
|
<div>Score {lead.score}</div>
|
||||||
|
<div>Sentiment {lead.sentiment_score}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!loading && scatter.length === 0 && <p className="text-sm text-white/50">No lead analytics available yet.</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4">
|
||||||
|
<div className="text-xs uppercase tracking-[0.18em] text-white/45">Lead Sources</div>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{(demographics?.by_source ?? []).map((row) => (
|
||||||
|
<div key={row.source} className="flex items-center justify-between text-sm text-white/80">
|
||||||
|
<span>{row.source}</span>
|
||||||
|
<span>{row.lead_count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4">
|
||||||
|
<div className="text-xs uppercase tracking-[0.18em] text-white/45">Qualification Mix</div>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{(demographics?.by_qualification ?? []).map((row) => (
|
||||||
|
<div key={row.qualification} className="flex items-center justify-between text-sm text-white/80">
|
||||||
|
<span>{row.qualification}</span>
|
||||||
|
<span>{row.lead_count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Platform Status and Verification"
|
||||||
|
subtitle="Production-readiness controls kept inside the same vertical marketing surface."
|
||||||
|
icon={DatabaseZap}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_auto]">
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4 text-sm text-white/75">
|
||||||
|
<div className="mb-1 text-white font-medium">CRM Analytics</div>
|
||||||
|
<div>{totals.totalLeads > 0 ? 'Live data available' : 'No seeded verification data yet'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4 text-sm text-white/75">
|
||||||
|
<div className="mb-1 text-white font-medium">Catalyst Contracts</div>
|
||||||
|
<div>{campaigns.length > 0 ? 'Marketing tab wired to root endpoints' : 'Campaign summary unavailable'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/8 bg-white/5 p-4 text-sm text-white/75">
|
||||||
|
<div className="mb-1 text-white font-medium">Spend Capacity</div>
|
||||||
|
<div>Total budget {formatMoney(totals.totalBudget)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleSeed()}
|
||||||
|
disabled={seeding}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl border border-blue-400/25 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{seeding ? <RefreshCw className="h-4 w-4 animate-spin" /> : <DatabaseZap className="h-4 w-4" />}
|
||||||
|
Seed 100 Synthetic Leads
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="mt-4 text-sm text-red-300">{error}</p>}
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
app/src/hooks/useCrmBootstrap.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { getChatLogs, getLeads } from '@/lib/api';
|
||||||
|
import { mapLeadRecordToStoreLead } from '@/lib/crmMappers';
|
||||||
|
import { useStore } from '@/store/useStore';
|
||||||
|
import type { ChatMessage } from '@/types';
|
||||||
|
|
||||||
|
export function useCrmBootstrap() {
|
||||||
|
const { setLeads, replaceMessages } = useStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const hydrate = async () => {
|
||||||
|
try {
|
||||||
|
const leads = await getLeads();
|
||||||
|
if (cancelled) return;
|
||||||
|
setLeads(leads.map(mapLeadRecordToStoreLead));
|
||||||
|
|
||||||
|
const messageEntries = await Promise.all(
|
||||||
|
leads.slice(0, 25).map(async (lead) => {
|
||||||
|
const logs = await getChatLogs(lead.id);
|
||||||
|
return [
|
||||||
|
lead.id,
|
||||||
|
logs.map((log): ChatMessage => ({
|
||||||
|
id: log.id,
|
||||||
|
sender: log.sender === 'lead' ? 'user' : 'oracle',
|
||||||
|
content: log.content,
|
||||||
|
timestamp: new Date(log.created_at ?? Date.now()),
|
||||||
|
})),
|
||||||
|
] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!cancelled) {
|
||||||
|
replaceMessages(Object.fromEntries(messageEntries));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep the current in-app fallback state if the CRM backend is unreachable.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void hydrate();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [replaceMessages, setLeads]);
|
||||||
|
}
|
||||||
@@ -17,3 +17,119 @@ export const API_URL = (
|
|||||||
).replace(/\/$/, '');
|
).replace(/\/$/, '');
|
||||||
|
|
||||||
export const WS_URL = API_URL.replace(/^http/, 'ws');
|
export const WS_URL = API_URL.replace(/^http/, 'ws');
|
||||||
|
|
||||||
|
export interface ScatterDataPoint {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sentiment_score: number;
|
||||||
|
response_time_ms: number;
|
||||||
|
score: number;
|
||||||
|
qualification: string;
|
||||||
|
kanban_status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeadRecord {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
source: string;
|
||||||
|
notes: string;
|
||||||
|
qualification: string;
|
||||||
|
score: number;
|
||||||
|
kanban_status: string;
|
||||||
|
stage: string;
|
||||||
|
budget: string;
|
||||||
|
unit_interest: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
created_at?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeadDemographics {
|
||||||
|
by_source: Array<{ source: string; lead_count: number; avg_score: number }>;
|
||||||
|
by_qualification: Array<{ qualification: string; lead_count: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatLogRecord {
|
||||||
|
id: string;
|
||||||
|
lead_id: string;
|
||||||
|
sender: string;
|
||||||
|
channel: string;
|
||||||
|
content: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
created_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketingCampaignSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
platform: 'meta' | 'google';
|
||||||
|
status: 'active' | 'paused' | 'completed';
|
||||||
|
budget: number;
|
||||||
|
spent: number;
|
||||||
|
impressions: number;
|
||||||
|
clicks: number;
|
||||||
|
conversions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJson<T>(path: string): Promise<T> {
|
||||||
|
const response = await fetch(`${API_URL}${path}`, {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestWrappedData<T>(path: string): Promise<T> {
|
||||||
|
const payload = await requestJson<{ data: T }>(path);
|
||||||
|
return payload.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSentimentScatter(): Promise<ScatterDataPoint[]> {
|
||||||
|
return requestJson<ScatterDataPoint[]>('/api/analytics/sentiment-scatter');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCatalystCampaigns(): Promise<MarketingCampaignSummary[]> {
|
||||||
|
return requestWrappedData<MarketingCampaignSummary[]>('/api/catalyst/campaigns');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLeads(): Promise<LeadRecord[]> {
|
||||||
|
const payload = await requestJson<{ data: LeadRecord[] }>('/api/leads');
|
||||||
|
return payload.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLead(leadId: string): Promise<LeadRecord> {
|
||||||
|
return requestWrappedData<LeadRecord>(`/api/leads/${leadId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKanbanBoard() {
|
||||||
|
return requestWrappedData<Array<{ status: string; stage: string; count: number; items: LeadRecord[] }>>('/api/kanban/board');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChatLogs(leadId?: string): Promise<ChatLogRecord[]> {
|
||||||
|
const suffix = leadId ? `?lead_id=${encodeURIComponent(leadId)}` : '';
|
||||||
|
return requestWrappedData<ChatLogRecord[]>(`/api/chat-logs${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLeadDemographics(): Promise<LeadDemographics> {
|
||||||
|
return requestWrappedData<LeadDemographics>('/api/leads/demographics');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedSyntheticLeads(count = 100): Promise<{ seeded: number; chat_logs_seeded: number; batch: string }> {
|
||||||
|
const response = await fetch(`${API_URL}/api/leads/seed-synthetic`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ count }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Seed request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
const payload = await response.json() as { data: { seeded: number; chat_logs_seeded: number; batch: string } };
|
||||||
|
return payload.data;
|
||||||
|
}
|
||||||
|
|||||||
122
app/src/lib/crmMappers.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import type { ChatLogRecord, LeadRecord } from '@/lib/api';
|
||||||
|
import type { Lead } from '@/types';
|
||||||
|
import type { LeadBadge, LeadTag, LeadSource, Message, MessageSender, PipelineStage, SentimentLog } from '@/types/crm';
|
||||||
|
|
||||||
|
const TAG_MAP: Record<string, LeadTag> = {
|
||||||
|
whale: '#CashBuyer',
|
||||||
|
potential: '#Investor',
|
||||||
|
hot: '#EndUser',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapLeadRecordToStoreLead(record: LeadRecord): Lead {
|
||||||
|
const qualification = record.qualification.toLowerCase() as Lead['qualification'];
|
||||||
|
const status = record.stage === 'closed'
|
||||||
|
? 'closed'
|
||||||
|
: record.stage === 'qualified' || record.stage === 'negotiation'
|
||||||
|
? 'qualified'
|
||||||
|
: record.score >= 75
|
||||||
|
? 'hot'
|
||||||
|
: record.stage === 'new'
|
||||||
|
? 'new'
|
||||||
|
: 'engaged';
|
||||||
|
const tags = Array.isArray(record.metadata?.tags) ? (record.metadata.tags as string[]) : [];
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
phone: record.phone ?? '',
|
||||||
|
source: mapSource(record.source),
|
||||||
|
status,
|
||||||
|
lastMessage: record.notes || 'No conversation summary yet.',
|
||||||
|
lastActive: new Date(record.updated_at ?? record.created_at ?? Date.now()),
|
||||||
|
unreadCount: 0,
|
||||||
|
qualification: qualification === 'tire_kicker' || qualification === 'potential' || qualification === 'whale'
|
||||||
|
? qualification
|
||||||
|
: 'potential',
|
||||||
|
budget: record.budget,
|
||||||
|
interest: record.unit_interest,
|
||||||
|
quantumDynamicsScore: record.score,
|
||||||
|
tags: tags.length > 0 ? tags : [record.qualification],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapLeadRecordToOracleLead(record: LeadRecord, chatLogs: ChatLogRecord[]): import('@/types/crm').Lead {
|
||||||
|
const badge = mapBadge(record.qualification);
|
||||||
|
const tags = mapOracleTags(record.qualification, record.metadata);
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
phone: record.phone ?? '',
|
||||||
|
stage: mapPipelineStage(record.stage),
|
||||||
|
oracleScore: record.score,
|
||||||
|
badge,
|
||||||
|
tags,
|
||||||
|
source: mapSource(record.source),
|
||||||
|
budget: record.budget,
|
||||||
|
unitInterest: record.unit_interest,
|
||||||
|
profileImageUrl: `https://api.dicebear.com/9.x/glass/svg?seed=${encodeURIComponent(record.name)}`,
|
||||||
|
visitedShowroom: record.stage === 'site_visit' || record.stage === 'negotiation' || record.stage === 'closed',
|
||||||
|
inShowroomNow: record.stage === 'site_visit',
|
||||||
|
messages: chatLogs.map(mapChatLogToOracleMessage),
|
||||||
|
sentimentLog: buildSentimentLog(record.score, record.stage),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSource(source: string): LeadSource {
|
||||||
|
if (source === 'walkin' || source === 'website' || source === 'whatsapp') return source;
|
||||||
|
return 'website';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPipelineStage(stage: string): PipelineStage {
|
||||||
|
const normalized = stage.toLowerCase();
|
||||||
|
if (normalized === 'new' || normalized === 'new_inquiries') return 'new_inquiries';
|
||||||
|
if (normalized === 'qualified' || normalized === 'qualifying') return 'qualified';
|
||||||
|
if (normalized === 'site_visit') return 'site_visit';
|
||||||
|
if (normalized === 'negotiation') return 'negotiation';
|
||||||
|
return 'closed';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapBadge(qualification: string): LeadBadge | undefined {
|
||||||
|
const normalized = qualification.toLowerCase();
|
||||||
|
if (normalized === 'whale') return 'whale';
|
||||||
|
if (normalized === 'hot' || normalized === 'potential') return 'hot';
|
||||||
|
if (normalized === 'tire_kicker') return 'tire_kicker';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOracleTags(qualification: string, metadata: Record<string, unknown>): LeadTag[] {
|
||||||
|
const mapped = TAG_MAP[qualification.toLowerCase()];
|
||||||
|
const rawTags = Array.isArray(metadata?.tags) ? metadata.tags as string[] : [];
|
||||||
|
const canonical = rawTags.includes('#CashBuyer') || mapped === '#CashBuyer'
|
||||||
|
? '#CashBuyer'
|
||||||
|
: rawTags.includes('#EndUser') || mapped === '#EndUser'
|
||||||
|
? '#EndUser'
|
||||||
|
: '#Investor';
|
||||||
|
return [canonical];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapChatLogToOracleMessage(log: ChatLogRecord): Message {
|
||||||
|
return {
|
||||||
|
id: log.id,
|
||||||
|
sender: mapSender(log.sender),
|
||||||
|
content: log.content,
|
||||||
|
createdAt: log.created_at ?? new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSender(sender: string): MessageSender {
|
||||||
|
if (sender === 'lead' || sender === 'oracle' || sender === 'system') return sender;
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSentimentLog(score: number, stage: string): SentimentLog[] {
|
||||||
|
const base = Math.max(20, score - 18);
|
||||||
|
const labels = stage === 'site_visit'
|
||||||
|
? ['Entry', 'Showroom peak', 'Pricing review']
|
||||||
|
: ['Discovery', 'Qualification', 'Follow-up'];
|
||||||
|
return labels.map((label, index) => ({
|
||||||
|
id: `${stage}-${index}`,
|
||||||
|
at: `${10 + index}:0${index}`,
|
||||||
|
score: Math.min(100, base + index * 9),
|
||||||
|
note: label,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ interface MarketingState {
|
|||||||
adInsights: AdInsight[];
|
adInsights: AdInsight[];
|
||||||
liveEvents: LiveOptimizationEvent[];
|
liveEvents: LiveOptimizationEvent[];
|
||||||
settings: CatalystSettings;
|
settings: CatalystSettings;
|
||||||
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room';
|
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
addCampaign: (campaign: Campaign) => void;
|
addCampaign: (campaign: Campaign) => void;
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface OracleState {
|
|||||||
activeLeadId: string | null;
|
activeLeadId: string | null;
|
||||||
messages: Record<string, ChatMessage[]>;
|
messages: Record<string, ChatMessage[]>;
|
||||||
isOracleThinking: boolean;
|
isOracleThinking: boolean;
|
||||||
|
setLeads: (leads: Lead[]) => void;
|
||||||
|
replaceMessages: (messages: Record<string, ChatMessage[]>) => void;
|
||||||
setActiveLead: (leadId: string | null) => void;
|
setActiveLead: (leadId: string | null) => void;
|
||||||
addMessage: (leadId: string, message: ChatMessage) => void;
|
addMessage: (leadId: string, message: ChatMessage) => void;
|
||||||
setOracleThinking: (thinking: boolean) => void;
|
setOracleThinking: (thinking: boolean) => void;
|
||||||
@@ -274,6 +276,8 @@ export const useStore = create<StoreState>()(
|
|||||||
activeLeadId: null,
|
activeLeadId: null,
|
||||||
messages: mockMessages,
|
messages: mockMessages,
|
||||||
isOracleThinking: false,
|
isOracleThinking: false,
|
||||||
|
setLeads: (leads) => set({ leads }),
|
||||||
|
replaceMessages: (messages) => set({ messages }),
|
||||||
setActiveLead: (leadId) => set({ activeLeadId: leadId }),
|
setActiveLead: (leadId) => set({ activeLeadId: leadId }),
|
||||||
addMessage: (leadId, message) => set((state) => ({
|
addMessage: (leadId, message) => set((state) => ({
|
||||||
messages: {
|
messages: {
|
||||||
|
|||||||
BIN
backend/api/__pycache__/routes_crm.cpython-314.pyc
Normal file
BIN
backend/api/__pycache__/routes_oracle.cpython-314.pyc
Normal file
@@ -11,14 +11,23 @@ Routes:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, status
|
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from backend.services.ad_network_service import (
|
||||||
|
AdInsight,
|
||||||
|
BidStrategyUpdate,
|
||||||
|
BudgetUpdate,
|
||||||
|
Platform,
|
||||||
|
ad_network_service,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -91,6 +100,7 @@ def _sha256_hash(value: str) -> str:
|
|||||||
|
|
||||||
class CampaignCreateRequest(BaseModel):
|
class CampaignCreateRequest(BaseModel):
|
||||||
name: str = Field(..., description="Campaign display name")
|
name: str = Field(..., description="Campaign display name")
|
||||||
|
platform: Platform = Field(default=Platform.META, description="Target ad network platform")
|
||||||
objective: str = Field("OUTCOME_LEADS", description="Meta campaign objective enum")
|
objective: str = Field("OUTCOME_LEADS", description="Meta campaign objective enum")
|
||||||
budget_daily: int = Field(..., gt=0, description="Daily budget in cents (AED × 100)")
|
budget_daily: int = Field(..., gt=0, description="Daily budget in cents (AED × 100)")
|
||||||
status: str = Field("PAUSED", description="Initial campaign status — start PAUSED for review")
|
status: str = Field("PAUSED", description="Initial campaign status — start PAUSED for review")
|
||||||
@@ -121,9 +131,55 @@ class MetaAuthRequest(BaseModel):
|
|||||||
short_lived_token: str = Field(..., description="Short-lived user access token from Meta OAuth")
|
short_lived_token: str = Field(..., description="Short-lived user access token from Meta OAuth")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/campaigns", summary="List unified campaign summaries for the Catalyst marketing tab")
|
||||||
|
async def list_campaigns(platform: Platform | None = Query(default=None)) -> dict:
|
||||||
|
campaigns = await ad_network_service.list_campaigns(platform=platform)
|
||||||
|
insights = await ad_network_service.get_insights(platform=platform, days=7)
|
||||||
|
rollup: dict[str, dict[str, float]] = {}
|
||||||
|
for insight in insights:
|
||||||
|
insight_campaign_id = insight.campaign_id if isinstance(insight, AdInsight) else insight.get("campaign_id")
|
||||||
|
if not insight_campaign_id:
|
||||||
|
continue
|
||||||
|
spent = insight.spend if isinstance(insight, AdInsight) else float(insight.get("spend", 0))
|
||||||
|
impressions = insight.impressions if isinstance(insight, AdInsight) else int(insight.get("impressions", 0))
|
||||||
|
clicks = insight.clicks if isinstance(insight, AdInsight) else int(insight.get("clicks", 0))
|
||||||
|
conversions = insight.conversions if isinstance(insight, AdInsight) else int(insight.get("conversions", 0))
|
||||||
|
slot = rollup.setdefault(
|
||||||
|
insight_campaign_id,
|
||||||
|
{
|
||||||
|
"spent": 0.0,
|
||||||
|
"impressions": 0.0,
|
||||||
|
"clicks": 0.0,
|
||||||
|
"conversions": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
slot["spent"] += spent
|
||||||
|
slot["impressions"] += impressions
|
||||||
|
slot["clicks"] += clicks
|
||||||
|
slot["conversions"] += conversions
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"id": campaign.id,
|
||||||
|
"name": campaign.name,
|
||||||
|
"platform": campaign.platform.value,
|
||||||
|
"status": campaign.status.value,
|
||||||
|
"budget": campaign.daily_budget,
|
||||||
|
"spent": round(rollup.get(campaign.id, {}).get("spent", campaign.spent), 2),
|
||||||
|
"impressions": int(rollup.get(campaign.id, {}).get("impressions", 0)),
|
||||||
|
"clicks": int(rollup.get(campaign.id, {}).get("clicks", 0)),
|
||||||
|
"conversions": int(rollup.get(campaign.id, {}).get("conversions", 0)),
|
||||||
|
"objective": campaign.objective,
|
||||||
|
"bid_strategy": campaign.bid_strategy,
|
||||||
|
}
|
||||||
|
for campaign in campaigns
|
||||||
|
]
|
||||||
|
source = "ad_network_service_live" if platform else "ad_network_service_unified"
|
||||||
|
return _ok(data, meta={"count": len(data), "source": source})
|
||||||
|
|
||||||
|
|
||||||
# ── 1. POST /campaigns/create ─────────────────────────────────────────────────
|
# ── 1. POST /campaigns/create ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.post("/campaigns/create", summary="Bulk-create Meta Marketing campaigns")
|
@router.post("/campaigns/create", summary="Create Meta or Google marketing campaigns")
|
||||||
async def create_campaigns(
|
async def create_campaigns(
|
||||||
request: Request,
|
request: Request,
|
||||||
payload: CampaignCreateRequest,
|
payload: CampaignCreateRequest,
|
||||||
@@ -134,6 +190,25 @@ async def create_campaigns(
|
|||||||
|
|
||||||
Requires: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID
|
Requires: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID
|
||||||
"""
|
"""
|
||||||
|
if payload.platform == Platform.GOOGLE:
|
||||||
|
campaign_id = f"google-camp-{uuid.uuid4().hex[:8]}"
|
||||||
|
if hasattr(request.app.state, "broadcast_live_event"):
|
||||||
|
await request.app.state.broadcast_live_event(
|
||||||
|
"create",
|
||||||
|
f"Created Google Ads campaign '{payload.name}'.",
|
||||||
|
payload.name,
|
||||||
|
f"Budget: AED {payload.budget_daily / 100:.0f}/day",
|
||||||
|
)
|
||||||
|
return _ok(
|
||||||
|
CampaignCreateResponse(
|
||||||
|
campaign_id=campaign_id,
|
||||||
|
name=payload.name,
|
||||||
|
status=payload.status,
|
||||||
|
created_at=datetime.utcnow().isoformat(),
|
||||||
|
).model_dump(),
|
||||||
|
meta={"platform": "google", "mode": "simulated_or_provider_managed"},
|
||||||
|
)
|
||||||
|
|
||||||
_api, account_id = _get_sdk()
|
_api, account_id = _get_sdk()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -226,53 +301,55 @@ async def sync_creative(
|
|||||||
|
|
||||||
# ── 3. GET /insights/realtime ─────────────────────────────────────────────────
|
# ── 3. GET /insights/realtime ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/insights/realtime", summary="Poll Meta Ads Insights API")
|
@router.get("/insights/realtime", summary="Poll unified Meta and Google Ads insights")
|
||||||
async def get_realtime_insights(
|
async def get_realtime_insights(
|
||||||
date_preset: str = "last_7_days",
|
campaign_id: str | None = None,
|
||||||
level: str = "adset",
|
platform: Platform | None = Query(default=None),
|
||||||
|
days: int = Query(default=7, ge=1, le=90),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
|
||||||
Polls `AdAccount.get_insights()` for CTR, CPA, spend, impressions across Ad Sets.
|
|
||||||
Supports `date_preset` (e.g. 'today', 'last_7_days', 'last_30_days') and
|
|
||||||
`level` ('campaign', 'adset', 'ad').
|
|
||||||
|
|
||||||
Requires: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID
|
|
||||||
"""
|
|
||||||
_api, account_id = _get_sdk()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from facebook_business.adobjects.adaccount import AdAccount # type: ignore
|
insights = await ad_network_service.get_insights(campaign_id=campaign_id, platform=platform, days=days)
|
||||||
from facebook_business.adobjects.adsinsights import AdsInsights # type: ignore
|
|
||||||
|
|
||||||
account = AdAccount(account_id)
|
|
||||||
fields = [
|
|
||||||
AdsInsights.Field.campaign_name,
|
|
||||||
AdsInsights.Field.adset_name,
|
|
||||||
AdsInsights.Field.spend,
|
|
||||||
AdsInsights.Field.impressions,
|
|
||||||
AdsInsights.Field.clicks,
|
|
||||||
AdsInsights.Field.ctr,
|
|
||||||
AdsInsights.Field.cpp, # cost per purchase (proxy for CPA)
|
|
||||||
AdsInsights.Field.date_start,
|
|
||||||
AdsInsights.Field.date_stop,
|
|
||||||
]
|
|
||||||
params = {
|
|
||||||
"date_preset": date_preset,
|
|
||||||
"level": level,
|
|
||||||
}
|
|
||||||
insights_cursor = account.get_insights(fields=fields, params=params)
|
|
||||||
results = [dict(row) for row in insights_cursor]
|
|
||||||
|
|
||||||
return _ok(results, meta={
|
|
||||||
"account_id": account_id,
|
|
||||||
"date_preset": date_preset,
|
|
||||||
"level": level,
|
|
||||||
"count": len(results),
|
|
||||||
})
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Insights fetch failed: %s", exc)
|
logger.error("Insights fetch failed: %s", exc)
|
||||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||||
|
|
||||||
|
data = [item.model_dump() if isinstance(item, AdInsight) else item for item in insights]
|
||||||
|
return _ok(data, meta={"count": len(data), "days": days, "platform": platform.value if platform else "all"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/budget", summary="Update Meta or Google Ads budget and campaign status")
|
||||||
|
async def update_campaign_budget(request: Request, payload: BudgetUpdate) -> dict:
|
||||||
|
try:
|
||||||
|
result = await ad_network_service.update_budget(payload)
|
||||||
|
if hasattr(request.app.state, "broadcast_live_event"):
|
||||||
|
await request.app.state.broadcast_live_event(
|
||||||
|
"budget_update",
|
||||||
|
f"Updated {payload.platform.value} budget for {payload.campaign_id}.",
|
||||||
|
payload.campaign_id,
|
||||||
|
f"daily={payload.daily_budget} lifetime={payload.lifetime_budget}",
|
||||||
|
)
|
||||||
|
return _ok(result)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Budget update failed: %s", exc)
|
||||||
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/bid-strategy", summary="Apply Meta or Google Ads bid strategy changes")
|
||||||
|
async def update_bid_strategy(request: Request, payload: BidStrategyUpdate) -> dict:
|
||||||
|
try:
|
||||||
|
action = await ad_network_service.update_bid_strategy(payload)
|
||||||
|
if hasattr(request.app.state, "broadcast_live_event"):
|
||||||
|
await request.app.state.broadcast_live_event(
|
||||||
|
"bid_strategy_update",
|
||||||
|
f"Updated {payload.platform.value} bid strategy for {payload.campaign_id}.",
|
||||||
|
payload.campaign_id,
|
||||||
|
payload.strategy,
|
||||||
|
)
|
||||||
|
return _ok(action.model_dump())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Bid strategy update failed: %s", exc)
|
||||||
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
# ── 4. POST /audiences/lookalike ──────────────────────────────────────────────
|
# ── 4. POST /audiences/lookalike ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,630 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
crm_router = APIRouter()
|
||||||
|
analytics_router = APIRouter()
|
||||||
|
|
||||||
|
_CRM_SCHEMA_CACHE_KEY = "_crm_schema_ready"
|
||||||
|
_KANBAN_STAGE_MAP = {
|
||||||
|
"new": "New",
|
||||||
|
"new_inquiries": "New",
|
||||||
|
"qualifying": "Qualifying",
|
||||||
|
"qualified": "Qualifying",
|
||||||
|
"site_visit": "Site Visit",
|
||||||
|
"site visit": "Site Visit",
|
||||||
|
"negotiation": "Negotiation",
|
||||||
|
"closed": "Closed",
|
||||||
|
"closed_won": "Closed",
|
||||||
|
"closed/won": "Closed",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_stage(value: str | None) -> str:
|
||||||
|
if not value:
|
||||||
|
return "New"
|
||||||
|
return _KANBAN_STAGE_MAP.get(value.strip().lower(), value.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_key(value: str) -> str:
|
||||||
|
stage = _normalize_stage(value)
|
||||||
|
return stage.lower().replace(" ", "_")
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_qualification(score: int | None, source: str | None, notes: str | None) -> str:
|
||||||
|
joined = f"{source or ''} {notes or ''}".lower()
|
||||||
|
if score is None:
|
||||||
|
return "UNKNOWN"
|
||||||
|
if score >= 90 or "cash" in joined or "hnw" in joined or "family office" in joined:
|
||||||
|
return "WHALE"
|
||||||
|
if score >= 70:
|
||||||
|
return "POTENTIAL"
|
||||||
|
if score >= 45:
|
||||||
|
return "HOT"
|
||||||
|
return "TIRE_KICKER"
|
||||||
|
|
||||||
|
|
||||||
|
async def _broadcast_crm_event(request: Request, payload: dict[str, Any]) -> None:
|
||||||
|
broadcaster = getattr(request.app.state, "broadcast_crm_event", None)
|
||||||
|
if broadcaster is not None:
|
||||||
|
await broadcaster(payload)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_pool(request: Request):
|
||||||
|
pool = getattr(request.app.state, "db_pool", None)
|
||||||
|
if pool is None:
|
||||||
|
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_schema(request: Request) -> None:
|
||||||
|
if getattr(request.app.state, _CRM_SCHEMA_CACHE_KEY, False):
|
||||||
|
return
|
||||||
|
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS leads (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
source TEXT NOT NULL DEFAULT 'website',
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
qualification TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||||
|
score INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kanban_status TEXT NOT NULL DEFAULT 'New',
|
||||||
|
budget TEXT NOT NULL DEFAULT '',
|
||||||
|
unit_interest TEXT NOT NULL DEFAULT '',
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_logs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
lead_id TEXT NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||||
|
sender TEXT NOT NULL,
|
||||||
|
channel TEXT NOT NULL DEFAULT 'oracle',
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await conn.execute("CREATE INDEX IF NOT EXISTS idx_leads_stage ON leads (kanban_status)")
|
||||||
|
await conn.execute("CREATE INDEX IF NOT EXISTS idx_leads_score ON leads (score DESC)")
|
||||||
|
await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_logs_lead_id ON chat_logs (lead_id, created_at DESC)")
|
||||||
|
|
||||||
|
setattr(request.app.state, _CRM_SCHEMA_CACHE_KEY, True)
|
||||||
|
|
||||||
|
|
||||||
|
class LeadUpsertRequest(BaseModel):
|
||||||
|
name: str = Field(..., min_length=1, max_length=200)
|
||||||
|
email: str | None = Field(default=None, max_length=255)
|
||||||
|
phone: str | None = Field(default=None, max_length=64)
|
||||||
|
source: str = Field(default="website", max_length=64)
|
||||||
|
notes: str = Field(default="", max_length=5000)
|
||||||
|
qualification: str | None = Field(default=None, max_length=64)
|
||||||
|
score: int = Field(default=0, ge=0, le=100)
|
||||||
|
kanban_status: str = Field(default="New", max_length=64)
|
||||||
|
budget: str = Field(default="", max_length=255)
|
||||||
|
unit_interest: str = Field(default="", max_length=255)
|
||||||
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class KanbanMoveRequest(BaseModel):
|
||||||
|
lead_id: str
|
||||||
|
target_status: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChatLogCreateRequest(BaseModel):
|
||||||
|
lead_id: str
|
||||||
|
sender: Literal["lead", "oracle", "system", "broker"] = "oracle"
|
||||||
|
channel: str = Field(default="oracle", max_length=64)
|
||||||
|
content: str = Field(..., min_length=1, max_length=8000)
|
||||||
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class SyntheticSeedRequest(BaseModel):
|
||||||
|
count: int = Field(default=100, ge=1, le=500)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_lead(row: Any) -> dict[str, Any]:
|
||||||
|
score = int(row["score"] or 0)
|
||||||
|
status_label = _normalize_stage(row["kanban_status"])
|
||||||
|
qualification = row["qualification"] or _infer_qualification(score, row.get("source"), row.get("notes"))
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"email": row["email"],
|
||||||
|
"phone": row["phone"],
|
||||||
|
"source": row["source"],
|
||||||
|
"notes": row["notes"],
|
||||||
|
"qualification": qualification,
|
||||||
|
"score": score,
|
||||||
|
"kanban_status": status_label,
|
||||||
|
"stage": _stage_key(status_label),
|
||||||
|
"budget": row["budget"],
|
||||||
|
"unit_interest": row["unit_interest"],
|
||||||
|
"metadata": row["metadata"] or {},
|
||||||
|
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
||||||
|
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_chat_log(row: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"lead_id": row["lead_id"],
|
||||||
|
"sender": row["sender"],
|
||||||
|
"channel": row["channel"],
|
||||||
|
"content": row["content"],
|
||||||
|
"metadata": row["metadata"] or {},
|
||||||
|
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_synthetic_leads(count: int) -> list[dict[str, Any]]:
|
||||||
|
first_names = ["Amina", "Omar", "Farah", "Rayan", "Maya", "Khalid", "Noor", "Zara", "Ibrahim", "Layla"]
|
||||||
|
last_names = ["Rahman", "Al-Farsi", "Kapoor", "Haddad", "Mehta", "Nadeem", "Shaikh", "Rao", "Wilson", "Chen"]
|
||||||
|
sources = ["website", "walkin", "whatsapp"]
|
||||||
|
stages = ["New", "Qualifying", "Site Visit", "Negotiation", "Closed"]
|
||||||
|
interests = ["2BHK Marina View", "3BHK Corner Unit", "Penthouse Sky Deck", "Investment Studio", "4BHK Sea View"]
|
||||||
|
budgets = ["AED 2.4M", "AED 4.8M", "AED 7.2M", "AED 12M", "AED 18M"]
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for idx in range(count):
|
||||||
|
score = 35 + ((idx * 7) % 61)
|
||||||
|
if idx % 12 == 0:
|
||||||
|
score = 94
|
||||||
|
name = f"{first_names[idx % len(first_names)]} {last_names[(idx * 3) % len(last_names)]}"
|
||||||
|
source = sources[idx % len(sources)]
|
||||||
|
notes = (
|
||||||
|
"Cash-ready HNI buyer focusing on waterfront premium inventory."
|
||||||
|
if score >= 90
|
||||||
|
else "Follow-up required on payment plan and amenity preferences."
|
||||||
|
)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": name,
|
||||||
|
"email": f"{name.lower().replace(' ', '.')}@synthetic.velocity.local",
|
||||||
|
"phone": f"+9715000{idx:05d}",
|
||||||
|
"source": source,
|
||||||
|
"notes": notes,
|
||||||
|
"qualification": _infer_qualification(score, source, notes).upper(),
|
||||||
|
"score": score,
|
||||||
|
"kanban_status": stages[idx % len(stages)],
|
||||||
|
"budget": budgets[idx % len(budgets)],
|
||||||
|
"unit_interest": interests[idx % len(interests)],
|
||||||
|
"metadata": {
|
||||||
|
"synthetic": True,
|
||||||
|
"campaign": "verification-seed",
|
||||||
|
"batch": "sprint1-root-integration",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.get("/leads")
|
||||||
|
async def list_leads(
|
||||||
|
request: Request,
|
||||||
|
kanban_status: str | None = None,
|
||||||
|
qualification: str | None = None,
|
||||||
|
search: str | None = Query(default=None, min_length=1),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
clauses: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
if kanban_status:
|
||||||
|
params.append(_normalize_stage(kanban_status))
|
||||||
|
clauses.append(f"kanban_status = ${len(params)}")
|
||||||
|
if qualification:
|
||||||
|
params.append(qualification.upper())
|
||||||
|
clauses.append(f"qualification = ${len(params)}")
|
||||||
|
if search:
|
||||||
|
params.append(f"%{search.lower()}%")
|
||||||
|
clauses.append(f"(LOWER(name) LIKE ${len(params)} OR LOWER(COALESCE(email, '')) LIKE ${len(params)} OR LOWER(COALESCE(phone, '')) LIKE ${len(params)})")
|
||||||
|
|
||||||
|
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||||
|
query = f"""
|
||||||
|
SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
FROM leads
|
||||||
|
{where}
|
||||||
|
ORDER BY score DESC, updated_at DESC, created_at DESC
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(query, *params)
|
||||||
|
leads = [_serialize_lead(row) for row in rows]
|
||||||
|
return {"status": "ok", "data": leads, "meta": {"count": len(leads)}}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.post("/leads", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_lead(request: Request, payload: LeadUpsertRequest) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
lead_id = str(uuid.uuid4())
|
||||||
|
qualification = (payload.qualification or _infer_qualification(payload.score, payload.source, payload.notes)).upper()
|
||||||
|
stage = _normalize_stage(payload.kanban_status)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO leads (
|
||||||
|
id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
||||||
|
$10, $11, $12::jsonb, NOW(), NOW()
|
||||||
|
)
|
||||||
|
RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
""",
|
||||||
|
lead_id,
|
||||||
|
payload.name,
|
||||||
|
payload.email,
|
||||||
|
payload.phone,
|
||||||
|
payload.source,
|
||||||
|
payload.notes,
|
||||||
|
qualification,
|
||||||
|
payload.score,
|
||||||
|
stage,
|
||||||
|
payload.budget,
|
||||||
|
payload.unit_interest,
|
||||||
|
json.dumps(payload.metadata),
|
||||||
|
)
|
||||||
|
data = _serialize_lead(row)
|
||||||
|
await _broadcast_crm_event(request, {"type": "lead_created", "entity": "lead", "data": data})
|
||||||
|
return {"status": "ok", "data": data}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.put("/leads/{lead_id}")
|
||||||
|
async def update_lead(lead_id: str, request: Request, payload: LeadUpsertRequest) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
qualification = (payload.qualification or _infer_qualification(payload.score, payload.source, payload.notes)).upper()
|
||||||
|
stage = _normalize_stage(payload.kanban_status)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
UPDATE leads
|
||||||
|
SET name = $2,
|
||||||
|
email = $3,
|
||||||
|
phone = $4,
|
||||||
|
source = $5,
|
||||||
|
notes = $6,
|
||||||
|
qualification = $7,
|
||||||
|
score = $8,
|
||||||
|
kanban_status = $9,
|
||||||
|
budget = $10,
|
||||||
|
unit_interest = $11,
|
||||||
|
metadata = $12::jsonb,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
""",
|
||||||
|
lead_id,
|
||||||
|
payload.name,
|
||||||
|
payload.email,
|
||||||
|
payload.phone,
|
||||||
|
payload.source,
|
||||||
|
payload.notes,
|
||||||
|
qualification,
|
||||||
|
payload.score,
|
||||||
|
stage,
|
||||||
|
payload.budget,
|
||||||
|
payload.unit_interest,
|
||||||
|
json.dumps(payload.metadata),
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
|
||||||
|
data = _serialize_lead(row)
|
||||||
|
await _broadcast_crm_event(request, {"type": "lead_updated", "entity": "lead", "data": data})
|
||||||
|
return {"status": "ok", "data": data}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.delete("/leads/{lead_id}")
|
||||||
|
async def delete_lead(lead_id: str, request: Request) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
result = await conn.execute("DELETE FROM leads WHERE id = $1", lead_id)
|
||||||
|
if result.endswith("0"):
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
|
||||||
|
await _broadcast_crm_event(request, {"type": "lead_deleted", "entity": "lead", "entity_id": lead_id})
|
||||||
|
return {"status": "ok", "data": {"id": lead_id, "deleted": True}}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.post("/leads/seed-synthetic", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def seed_synthetic_leads(request: Request, payload: SyntheticSeedRequest) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
synthetic_rows = _build_synthetic_leads(payload.count)
|
||||||
|
inserted = 0
|
||||||
|
chat_logs_inserted = 0
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
for row in synthetic_rows:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO leads (
|
||||||
|
id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
||||||
|
$10, $11, $12::jsonb, NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING
|
||||||
|
""",
|
||||||
|
row["id"],
|
||||||
|
row["name"],
|
||||||
|
row["email"],
|
||||||
|
row["phone"],
|
||||||
|
row["source"],
|
||||||
|
row["notes"],
|
||||||
|
row["qualification"],
|
||||||
|
row["score"],
|
||||||
|
row["kanban_status"],
|
||||||
|
row["budget"],
|
||||||
|
row["unit_interest"],
|
||||||
|
json.dumps(row["metadata"]),
|
||||||
|
)
|
||||||
|
inserted += 1
|
||||||
|
for sender, channel, content in [
|
||||||
|
("lead", "whatsapp", f"{row['name']} asked for availability on {row['unit_interest']}."),
|
||||||
|
("oracle", "oracle", "Oracle generated a guided follow-up based on budget, stage, and source quality."),
|
||||||
|
]:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO chat_logs (id, lead_id, sender, channel, content, metadata, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, NOW())
|
||||||
|
""",
|
||||||
|
str(uuid.uuid4()),
|
||||||
|
row["id"],
|
||||||
|
sender,
|
||||||
|
channel,
|
||||||
|
content,
|
||||||
|
json.dumps({"synthetic": True}),
|
||||||
|
)
|
||||||
|
chat_logs_inserted += 1
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"seeded": inserted,
|
||||||
|
"chat_logs_seeded": chat_logs_inserted,
|
||||||
|
"batch": "sprint1-root-integration",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await _broadcast_crm_event(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"type": "crm_seeded",
|
||||||
|
"entity": "lead_batch",
|
||||||
|
"data": result["data"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.get("/leads/demographics")
|
||||||
|
async def lead_demographics(request: Request) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
source_rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT source, COUNT(*)::int AS lead_count, COALESCE(AVG(score), 0)::float AS avg_score
|
||||||
|
FROM leads
|
||||||
|
GROUP BY source
|
||||||
|
ORDER BY lead_count DESC, source ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
qualification_rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT qualification, COUNT(*)::int AS lead_count
|
||||||
|
FROM leads
|
||||||
|
GROUP BY qualification
|
||||||
|
ORDER BY lead_count DESC, qualification ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"by_source": [dict(row) for row in source_rows],
|
||||||
|
"by_qualification": [dict(row) for row in qualification_rows],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.get("/leads/{lead_id}")
|
||||||
|
async def get_lead(lead_id: str, request: Request) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
FROM leads
|
||||||
|
WHERE id = $1
|
||||||
|
""",
|
||||||
|
lead_id,
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
|
||||||
|
return {"status": "ok", "data": _serialize_lead(row)}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.get("/chat-logs")
|
||||||
|
async def list_chat_logs(
|
||||||
|
request: Request,
|
||||||
|
lead_id: str | None = None,
|
||||||
|
channel: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
clauses: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
if lead_id:
|
||||||
|
params.append(lead_id)
|
||||||
|
clauses.append(f"lead_id = ${len(params)}")
|
||||||
|
if channel:
|
||||||
|
params.append(channel)
|
||||||
|
clauses.append(f"channel = ${len(params)}")
|
||||||
|
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||||
|
query = f"""
|
||||||
|
SELECT id, lead_id, sender, channel, content, metadata, created_at
|
||||||
|
FROM chat_logs
|
||||||
|
{where}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(query, *params)
|
||||||
|
data = [_serialize_chat_log(row) for row in rows]
|
||||||
|
return {"status": "ok", "data": data, "meta": {"count": len(data)}}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.post("/chat-logs", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_chat_log(request: Request, payload: ChatLogCreateRequest) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
log_id = str(uuid.uuid4())
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
lead = await conn.fetchrow("SELECT id FROM leads WHERE id = $1", payload.lead_id)
|
||||||
|
if lead is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{payload.lead_id}' not found.")
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO chat_logs (id, lead_id, sender, channel, content, metadata, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, NOW())
|
||||||
|
RETURNING id, lead_id, sender, channel, content, metadata, created_at
|
||||||
|
""",
|
||||||
|
log_id,
|
||||||
|
payload.lead_id,
|
||||||
|
payload.sender,
|
||||||
|
payload.channel,
|
||||||
|
payload.content,
|
||||||
|
json.dumps(payload.metadata),
|
||||||
|
)
|
||||||
|
data = _serialize_chat_log(row)
|
||||||
|
await _broadcast_crm_event(request, {"type": "chat_log_created", "entity": "chat_log", "data": data})
|
||||||
|
return {"status": "ok", "data": data}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.get("/kanban/board")
|
||||||
|
async def get_kanban_board(request: Request) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
ordered_stages = ["New", "Qualifying", "Site Visit", "Negotiation", "Closed"]
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
FROM leads
|
||||||
|
ORDER BY score DESC, updated_at DESC, created_at DESC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
leads = [_serialize_lead(row) for row in rows]
|
||||||
|
grouped = {stage: [] for stage in ordered_stages}
|
||||||
|
for lead in leads:
|
||||||
|
grouped.setdefault(lead["kanban_status"], []).append(lead)
|
||||||
|
board = [
|
||||||
|
{
|
||||||
|
"status": stage,
|
||||||
|
"stage": _stage_key(stage),
|
||||||
|
"count": len(grouped.get(stage, [])),
|
||||||
|
"items": grouped.get(stage, []),
|
||||||
|
}
|
||||||
|
for stage in ordered_stages
|
||||||
|
]
|
||||||
|
return {"status": "ok", "data": board}
|
||||||
|
|
||||||
|
|
||||||
|
@crm_router.put("/kanban/move")
|
||||||
|
async def move_kanban_card(request: Request, payload: KanbanMoveRequest) -> dict[str, Any]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
stage = _normalize_stage(payload.target_status)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
UPDATE leads
|
||||||
|
SET kanban_status = $2,
|
||||||
|
qualification = CASE
|
||||||
|
WHEN score >= 90 THEN 'WHALE'
|
||||||
|
WHEN score >= 70 THEN 'POTENTIAL'
|
||||||
|
WHEN score >= 45 THEN 'HOT'
|
||||||
|
ELSE qualification
|
||||||
|
END,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
|
||||||
|
budget, unit_interest, metadata, created_at, updated_at
|
||||||
|
""",
|
||||||
|
payload.lead_id,
|
||||||
|
stage,
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{payload.lead_id}' not found.")
|
||||||
|
data = _serialize_lead(row)
|
||||||
|
await _broadcast_crm_event(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"type": "kanban_moved",
|
||||||
|
"entity": "lead",
|
||||||
|
"entity_id": payload.lead_id,
|
||||||
|
"data": data,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"status": "ok", "data": data}
|
||||||
|
|
||||||
|
|
||||||
|
@analytics_router.get("/sentiment-scatter")
|
||||||
|
async def sentiment_scatter(request: Request) -> list[dict[str, Any]]:
|
||||||
|
await _ensure_schema(request)
|
||||||
|
pool = await _get_pool(request)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT id, name, score, qualification, kanban_status, source, notes, updated_at
|
||||||
|
FROM leads
|
||||||
|
WHERE score IS NOT NULL
|
||||||
|
ORDER BY score DESC, updated_at DESC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
points: list[dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
score = int(row["score"] or 0)
|
||||||
|
qualification = row["qualification"] or _infer_qualification(score, row["source"], row["notes"])
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"id": row["id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"sentiment_score": max(0, min(100, int(score * 0.82) + 10)),
|
||||||
|
"response_time_ms": max(120, 10000 - (score * 55)),
|
||||||
|
"score": score,
|
||||||
|
"qualification": qualification,
|
||||||
|
"kanban_status": _normalize_stage(row["kanban_status"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return points
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from backend.oracle.action_service import oracle_action_service
|
||||||
|
from backend.oracle.persona_service import persona_service
|
||||||
|
from backend.services.mcp_registry import mcp_registry
|
||||||
|
from backend.services.nemoclaw_runtime import nemoclaw_runtime
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowPreviewRequest(BaseModel):
|
||||||
|
prompt: str = Field(..., min_length=1, max_length=4096)
|
||||||
|
tenant_id: str = "tenant_velocity"
|
||||||
|
actor_role: str = "sales_director"
|
||||||
|
|
||||||
|
|
||||||
|
class MCPExecuteRequest(BaseModel):
|
||||||
|
tool_name: str = Field(..., min_length=1, max_length=128)
|
||||||
|
query: str = Field(..., min_length=1, max_length=1024)
|
||||||
|
|
||||||
|
|
||||||
|
class OracleWritebackRequest(BaseModel):
|
||||||
|
action_id: str
|
||||||
|
tenant_id: str = "tenant_velocity"
|
||||||
|
actor_id: str = "oracle_operator"
|
||||||
|
target_entity_type: str = Field(..., min_length=1, max_length=64)
|
||||||
|
target_entity_id: str = Field(..., min_length=1, max_length=128)
|
||||||
|
action_type: str = Field(default="lead_writeback", min_length=1, max_length=128)
|
||||||
|
writeback_payload: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def oracle_health() -> dict:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"persona": await persona_service.health(),
|
||||||
|
"mcp_tools": mcp_registry.list_tools(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mcp/tools")
|
||||||
|
async def oracle_mcp_tools() -> dict:
|
||||||
|
return {"status": "ok", "data": mcp_registry.list_tools()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/mcp/execute")
|
||||||
|
async def oracle_mcp_execute(request: Request, payload: MCPExecuteRequest) -> dict:
|
||||||
|
pool = getattr(request.app.state, "db_pool", None)
|
||||||
|
result = await mcp_registry.execute(payload.tool_name, payload.query, crm_pool=pool)
|
||||||
|
return {"status": "ok", "data": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/workflow/preview")
|
||||||
|
async def workflow_preview(payload: WorkflowPreviewRequest) -> dict:
|
||||||
|
persona_plan = await persona_service.plan_for_prompt(
|
||||||
|
prompt=payload.prompt,
|
||||||
|
tenant_id=payload.tenant_id,
|
||||||
|
actor_role=payload.actor_role,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"persona_plan": persona_plan,
|
||||||
|
"workflow": nemoclaw_runtime.build_workflow_dispatch(
|
||||||
|
prompt=payload.prompt,
|
||||||
|
tenant_id=payload.tenant_id,
|
||||||
|
actor_role=payload.actor_role,
|
||||||
|
component_templates=persona_plan["recommendedTemplates"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/actions")
|
||||||
|
async def list_oracle_actions(status: str | None = None, limit: int = 50) -> dict:
|
||||||
|
actions = await oracle_action_service.list_actions(status=status, limit=limit)
|
||||||
|
return {"status": "ok", "data": actions, "meta": {"count": len(actions)}}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/actions/{action_id}")
|
||||||
|
async def get_oracle_action(action_id: str) -> dict:
|
||||||
|
action = await oracle_action_service.get_action(action_id)
|
||||||
|
if not action:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Oracle action '{action_id}' not found.")
|
||||||
|
return {"status": "ok", "data": action}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/actions/writeback")
|
||||||
|
async def apply_oracle_writeback(request: Request, payload: OracleWritebackRequest) -> dict:
|
||||||
|
result = await oracle_action_service.apply_writeback(payload.model_dump())
|
||||||
|
if hasattr(request.app.state, "broadcast_crm_event"):
|
||||||
|
await request.app.state.broadcast_crm_event(
|
||||||
|
{
|
||||||
|
"type": "oracle_writeback",
|
||||||
|
"entity": payload.target_entity_type,
|
||||||
|
"entity_id": payload.target_entity_id,
|
||||||
|
"action_id": payload.action_id,
|
||||||
|
"payload": result["resultPayload"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"status": "ok", "data": result}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
@@ -21,10 +21,13 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from backend.api.routes_catalyst import router as catalyst_router
|
from backend.api.routes_catalyst import router as catalyst_router
|
||||||
|
from backend.api.routes_crm import crm_router, analytics_router
|
||||||
|
from backend.api.routes_oracle import router as oracle_helper_router
|
||||||
from backend.auth.dependencies import (
|
from backend.auth.dependencies import (
|
||||||
create_access_token, verify_password, get_current_user
|
create_access_token, verify_password, get_current_user
|
||||||
)
|
)
|
||||||
from backend.db.pool import create_pool, close_pool
|
from backend.db.pool import create_pool, close_pool
|
||||||
|
from backend.oracle.router_v1 import router as oracle_v1_router
|
||||||
from backend.routers.cctv import router as cctv_router
|
from backend.routers.cctv import router as cctv_router
|
||||||
from backend.routers.scenes import router as scenes_router
|
from backend.routers.scenes import router as scenes_router
|
||||||
from backend.routers.videos import router as videos_router
|
from backend.routers.videos import router as videos_router
|
||||||
@@ -86,6 +89,10 @@ if os.path.isdir(ASSET_DIR):
|
|||||||
# ── Routers ───────────────────────────────────────────────────────────────────
|
# ── Routers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"])
|
app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"])
|
||||||
|
app.include_router(crm_router, prefix="/api", tags=["CRM"])
|
||||||
|
app.include_router(analytics_router, prefix="/api/analytics", tags=["Analytics"])
|
||||||
|
app.include_router(oracle_helper_router, prefix="/api/oracle", tags=["Oracle"])
|
||||||
|
app.include_router(oracle_v1_router, prefix="/api/oracle/v1", tags=["Oracle V1"])
|
||||||
app.include_router(sentinel_router, prefix="/api/sentinel", tags=["Sentinel"])
|
app.include_router(sentinel_router, prefix="/api/sentinel", tags=["Sentinel"])
|
||||||
app.include_router(cctv_router, prefix="/api/cctv", tags=["CCTV"])
|
app.include_router(cctv_router, prefix="/api/cctv", tags=["CCTV"])
|
||||||
app.include_router(scenes_router, prefix="/api/scenes", tags=["Scenes"])
|
app.include_router(scenes_router, prefix="/api/scenes", tags=["Scenes"])
|
||||||
@@ -165,6 +172,30 @@ class _CatalystManager:
|
|||||||
_catalyst_mgr = _CatalystManager()
|
_catalyst_mgr = _CatalystManager()
|
||||||
|
|
||||||
|
|
||||||
|
class _CRMManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.active: Set[WebSocket] = set()
|
||||||
|
|
||||||
|
async def connect(self, ws: WebSocket) -> None:
|
||||||
|
await ws.accept()
|
||||||
|
self.active.add(ws)
|
||||||
|
|
||||||
|
def disconnect(self, ws: WebSocket) -> None:
|
||||||
|
self.active.discard(ws)
|
||||||
|
|
||||||
|
async def broadcast(self, payload: dict) -> None:
|
||||||
|
dead: Set[WebSocket] = set()
|
||||||
|
for ws in self.active:
|
||||||
|
try:
|
||||||
|
await ws.send_text(json.dumps(payload))
|
||||||
|
except Exception:
|
||||||
|
dead.add(ws)
|
||||||
|
self.active -= dead
|
||||||
|
|
||||||
|
|
||||||
|
_crm_mgr = _CRMManager()
|
||||||
|
|
||||||
|
|
||||||
@app.websocket("/ws/catalyst")
|
@app.websocket("/ws/catalyst")
|
||||||
async def catalyst_ws(ws: WebSocket) -> None:
|
async def catalyst_ws(ws: WebSocket) -> None:
|
||||||
await _catalyst_mgr.connect(ws)
|
await _catalyst_mgr.connect(ws)
|
||||||
@@ -176,13 +207,31 @@ async def catalyst_ws(ws: WebSocket) -> None:
|
|||||||
_catalyst_mgr.disconnect(ws)
|
_catalyst_mgr.disconnect(ws)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws/crm")
|
||||||
|
async def crm_ws(ws: WebSocket) -> None:
|
||||||
|
await _crm_mgr.connect(ws)
|
||||||
|
await _crm_mgr.broadcast(
|
||||||
|
{
|
||||||
|
"type": "crm_presence",
|
||||||
|
"connected_clients": len(_crm_mgr.active),
|
||||||
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
message = await ws.receive_text()
|
||||||
|
await ws.send_text(json.dumps({"type": "crm_ack", "data": message}))
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
_crm_mgr.disconnect(ws)
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_live_event(event_type, message, campaign_name=None, value=None):
|
async def broadcast_live_event(event_type, message, campaign_name=None, value=None):
|
||||||
payload = {
|
payload = {
|
||||||
"type": event_type,
|
"type": event_type,
|
||||||
"message": message,
|
"message": message,
|
||||||
"campaignName": campaign_name,
|
"campaignName": campaign_name,
|
||||||
"value": value,
|
"value": value,
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
}
|
}
|
||||||
await _catalyst_mgr.broadcast(payload)
|
await _catalyst_mgr.broadcast(payload)
|
||||||
|
|
||||||
@@ -190,6 +239,17 @@ async def broadcast_live_event(event_type, message, campaign_name=None, value=No
|
|||||||
app.state.broadcast_live_event = broadcast_live_event
|
app.state.broadcast_live_event = broadcast_live_event
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_crm_event(payload: dict) -> None:
|
||||||
|
enriched = {
|
||||||
|
**payload,
|
||||||
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
|
}
|
||||||
|
await _crm_mgr.broadcast(enriched)
|
||||||
|
|
||||||
|
|
||||||
|
app.state.broadcast_crm_event = broadcast_crm_event
|
||||||
|
|
||||||
|
|
||||||
# ── Health ─────────────────────────────────────────────────────────────────────
|
# ── Health ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/health", tags=["Health"])
|
@app.get("/health", tags=["Health"])
|
||||||
@@ -201,6 +261,6 @@ async def health() -> dict:
|
|||||||
"service": "velocity-backend",
|
"service": "velocity-backend",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"db_pool": "connected" if db_ok else "unavailable",
|
"db_pool": "connected" if db_ok else "unavailable",
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
backend/oracle/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/action_service.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/canvas_service.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/collaboration_service.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/data_access_gateway.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/persona_service.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/policy_service.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/prompt_orchestrator.cpython-314.pyc
Normal file
BIN
backend/oracle/__pycache__/router_v1.cpython-314.pyc
Normal file
346
backend/oracle/action_service.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
try:
|
||||||
|
import asyncpg # type: ignore
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
asyncpg = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
_DB_URL = os.getenv("DATABASE_URL", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _db_ready() -> bool:
|
||||||
|
return bool(_DB_URL and not _DB_URL.startswith("PLACEHOLDER") and asyncpg is not None)
|
||||||
|
|
||||||
|
|
||||||
|
class OracleActionService:
|
||||||
|
async def ensure_schema(self) -> None:
|
||||||
|
if not _db_ready():
|
||||||
|
return
|
||||||
|
assert asyncpg is not None
|
||||||
|
conn = await asyncpg.connect(_DB_URL)
|
||||||
|
try:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS oracle_actions (
|
||||||
|
action_id UUID PRIMARY KEY,
|
||||||
|
execution_id UUID,
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
|
page_id UUID,
|
||||||
|
branch_id TEXT,
|
||||||
|
actor_id TEXT NOT NULL,
|
||||||
|
target_entity_type TEXT NOT NULL,
|
||||||
|
target_entity_id TEXT,
|
||||||
|
action_type TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'planned',
|
||||||
|
prompt TEXT,
|
||||||
|
workflow_dispatch JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
component_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
writeback_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
result_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_oracle_actions_execution ON oracle_actions(execution_id, created_at DESC)"
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_oracle_actions_target ON oracle_actions(target_entity_type, target_entity_id, created_at DESC)"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
async def create_from_execution(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
execution: dict[str, Any],
|
||||||
|
target_entity_type: str = "canvas_page",
|
||||||
|
target_entity_id: str | None = None,
|
||||||
|
action_type: str = "oracle_canvas_generation",
|
||||||
|
writeback_payload: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
action = {
|
||||||
|
"actionId": str(uuid.uuid4()),
|
||||||
|
"executionId": execution.get("executionId"),
|
||||||
|
"tenantId": execution.get("tenantId"),
|
||||||
|
"pageId": execution.get("pageId"),
|
||||||
|
"branchId": execution.get("branchId"),
|
||||||
|
"actorId": execution.get("actorId"),
|
||||||
|
"targetEntityType": target_entity_type,
|
||||||
|
"targetEntityId": target_entity_id or execution.get("pageId"),
|
||||||
|
"actionType": action_type,
|
||||||
|
"status": "planned",
|
||||||
|
"prompt": execution.get("prompt"),
|
||||||
|
"workflowDispatch": execution.get("workflowDispatch") or {},
|
||||||
|
"componentIds": execution.get("componentsCreated") or [],
|
||||||
|
"writebackPayload": writeback_payload or {},
|
||||||
|
"resultPayload": {},
|
||||||
|
"createdAt": _now(),
|
||||||
|
"updatedAt": _now(),
|
||||||
|
}
|
||||||
|
await self._persist_action(action)
|
||||||
|
return action
|
||||||
|
|
||||||
|
async def get_action(self, action_id: str) -> dict[str, Any] | None:
|
||||||
|
if not _db_ready():
|
||||||
|
return None
|
||||||
|
assert asyncpg is not None
|
||||||
|
conn = await asyncpg.connect(_DB_URL)
|
||||||
|
try:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
||||||
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
||||||
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM oracle_actions
|
||||||
|
WHERE action_id = $1::uuid
|
||||||
|
""",
|
||||||
|
action_id,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
return self._serialize(row) if row else None
|
||||||
|
|
||||||
|
async def list_actions(self, *, status: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
|
||||||
|
if not _db_ready():
|
||||||
|
return []
|
||||||
|
assert asyncpg is not None
|
||||||
|
conn = await asyncpg.connect(_DB_URL)
|
||||||
|
try:
|
||||||
|
if status:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
||||||
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
||||||
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM oracle_actions
|
||||||
|
WHERE status = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $2
|
||||||
|
""",
|
||||||
|
status,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
||||||
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
||||||
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM oracle_actions
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $1
|
||||||
|
""",
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
return [self._serialize(row) for row in rows]
|
||||||
|
|
||||||
|
async def apply_writeback(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if not _db_ready():
|
||||||
|
raise HTTPException(status_code=503, detail="Oracle writeback store unavailable.")
|
||||||
|
if payload["target_entity_type"] != "lead":
|
||||||
|
raise HTTPException(status_code=422, detail="Only lead writebacks are supported in this pass.")
|
||||||
|
|
||||||
|
assert asyncpg is not None
|
||||||
|
await self.ensure_schema()
|
||||||
|
conn = await asyncpg.connect(_DB_URL)
|
||||||
|
try:
|
||||||
|
target_lead_id = payload["target_entity_id"]
|
||||||
|
action_id = payload["action_id"]
|
||||||
|
writeback = payload["writeback_payload"]
|
||||||
|
|
||||||
|
existing = await conn.fetchrow(
|
||||||
|
"SELECT id, notes, metadata, kanban_status, qualification, score FROM leads WHERE id = $1",
|
||||||
|
target_lead_id,
|
||||||
|
)
|
||||||
|
if existing is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Lead '{target_lead_id}' not found for Oracle writeback.")
|
||||||
|
|
||||||
|
metadata = dict(existing["metadata"] or {})
|
||||||
|
metadata_patch = writeback.get("metadata_patch") or {}
|
||||||
|
if isinstance(metadata_patch, dict):
|
||||||
|
metadata.update(metadata_patch)
|
||||||
|
|
||||||
|
score = int(existing["score"] or 0) + int(writeback.get("score_delta") or 0)
|
||||||
|
updated_notes = (existing["notes"] or "").strip()
|
||||||
|
notes_append = writeback.get("notes_append")
|
||||||
|
if notes_append:
|
||||||
|
separator = "\n\n" if updated_notes else ""
|
||||||
|
updated_notes = f"{updated_notes}{separator}{notes_append}"
|
||||||
|
|
||||||
|
updated = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
UPDATE leads
|
||||||
|
SET notes = $2,
|
||||||
|
metadata = $3::jsonb,
|
||||||
|
kanban_status = COALESCE($4, kanban_status),
|
||||||
|
qualification = COALESCE($5, qualification),
|
||||||
|
score = $6,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, notes, metadata, kanban_status, qualification, score, updated_at
|
||||||
|
""",
|
||||||
|
target_lead_id,
|
||||||
|
updated_notes,
|
||||||
|
json.dumps(metadata),
|
||||||
|
writeback.get("kanban_status"),
|
||||||
|
writeback.get("qualification"),
|
||||||
|
max(score, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
oracle_message = writeback.get("oracle_message")
|
||||||
|
if oracle_message:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO chat_logs (id, lead_id, sender, channel, content, metadata, created_at)
|
||||||
|
VALUES ($1, $2, 'oracle', 'oracle', $3, $4::jsonb, NOW())
|
||||||
|
""",
|
||||||
|
str(uuid.uuid4()),
|
||||||
|
target_lead_id,
|
||||||
|
oracle_message,
|
||||||
|
json.dumps({"oracle_action_id": action_id, "writeback": True}),
|
||||||
|
)
|
||||||
|
|
||||||
|
result_payload = {
|
||||||
|
"lead_id": updated["id"],
|
||||||
|
"kanban_status": updated["kanban_status"],
|
||||||
|
"qualification": updated["qualification"],
|
||||||
|
"score": updated["score"],
|
||||||
|
"updated_at": updated["updated_at"].isoformat() if updated["updated_at"] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO oracle_actions (
|
||||||
|
action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
||||||
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
||||||
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
||||||
|
created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1::uuid, NULL, $2, NULL, NULL, $3,
|
||||||
|
$4, $5, $6, 'applied', NULL,
|
||||||
|
'{}'::jsonb, '[]'::jsonb, $7::jsonb, $8::jsonb,
|
||||||
|
NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (action_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
status = 'applied',
|
||||||
|
writeback_payload = EXCLUDED.writeback_payload,
|
||||||
|
result_payload = EXCLUDED.result_payload,
|
||||||
|
updated_at = NOW()
|
||||||
|
""",
|
||||||
|
action_id,
|
||||||
|
payload.get("tenant_id", "tenant_velocity"),
|
||||||
|
payload.get("actor_id", "oracle_operator"),
|
||||||
|
payload["target_entity_type"],
|
||||||
|
target_lead_id,
|
||||||
|
payload.get("action_type", "lead_writeback"),
|
||||||
|
json.dumps(writeback),
|
||||||
|
json.dumps(result_payload),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"actionId": action_id,
|
||||||
|
"status": "applied",
|
||||||
|
"targetEntityType": payload["target_entity_type"],
|
||||||
|
"targetEntityId": payload["target_entity_id"],
|
||||||
|
"resultPayload": result_payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _persist_action(self, action: dict[str, Any]) -> None:
|
||||||
|
if not _db_ready():
|
||||||
|
return
|
||||||
|
await self.ensure_schema()
|
||||||
|
assert asyncpg is not None
|
||||||
|
conn = await asyncpg.connect(_DB_URL)
|
||||||
|
try:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO oracle_actions (
|
||||||
|
action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
||||||
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
||||||
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
||||||
|
created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1::uuid, $2::uuid, $3, $4::uuid, $5, $6,
|
||||||
|
$7, $8, $9, $10, $11,
|
||||||
|
$12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb,
|
||||||
|
$16::timestamptz, $17::timestamptz
|
||||||
|
)
|
||||||
|
ON CONFLICT (action_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
workflow_dispatch = EXCLUDED.workflow_dispatch,
|
||||||
|
component_ids = EXCLUDED.component_ids,
|
||||||
|
writeback_payload = EXCLUDED.writeback_payload,
|
||||||
|
result_payload = EXCLUDED.result_payload,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
""",
|
||||||
|
action["actionId"],
|
||||||
|
action.get("executionId"),
|
||||||
|
action["tenantId"],
|
||||||
|
action.get("pageId"),
|
||||||
|
action.get("branchId"),
|
||||||
|
action["actorId"],
|
||||||
|
action["targetEntityType"],
|
||||||
|
action.get("targetEntityId"),
|
||||||
|
action["actionType"],
|
||||||
|
action["status"],
|
||||||
|
action.get("prompt"),
|
||||||
|
json.dumps(action.get("workflowDispatch") or {}),
|
||||||
|
json.dumps(action.get("componentIds") or []),
|
||||||
|
json.dumps(action.get("writebackPayload") or {}),
|
||||||
|
json.dumps(action.get("resultPayload") or {}),
|
||||||
|
action["createdAt"],
|
||||||
|
action["updatedAt"],
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize(row: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"actionId": str(row["action_id"]),
|
||||||
|
"executionId": str(row["execution_id"]) if row["execution_id"] else None,
|
||||||
|
"tenantId": row["tenant_id"],
|
||||||
|
"pageId": str(row["page_id"]) if row["page_id"] else None,
|
||||||
|
"branchId": row["branch_id"],
|
||||||
|
"actorId": row["actor_id"],
|
||||||
|
"targetEntityType": row["target_entity_type"],
|
||||||
|
"targetEntityId": row["target_entity_id"],
|
||||||
|
"actionType": row["action_type"],
|
||||||
|
"status": row["status"],
|
||||||
|
"prompt": row["prompt"],
|
||||||
|
"workflowDispatch": row["workflow_dispatch"] or {},
|
||||||
|
"componentIds": row["component_ids"] or [],
|
||||||
|
"writebackPayload": row["writeback_payload"] or {},
|
||||||
|
"resultPayload": row["result_payload"] or {},
|
||||||
|
"createdAt": row["created_at"].isoformat() if row["created_at"] else None,
|
||||||
|
"updatedAt": row["updated_at"].isoformat() if row["updated_at"] else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
oracle_action_service = OracleActionService()
|
||||||
97
backend/oracle/persona_service.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
_PROMPT_DIR = Path(__file__).resolve().parent.parent / "nemoclaw_prompts"
|
||||||
|
_PLACEHOLDER_PATTERN = re.compile(r"\{(\w+)\}")
|
||||||
|
_TEMPLATE_HINTS = {
|
||||||
|
"pipeline": ["tpl_pipeline_board_v2", "tpl_followup_queue_v1"],
|
||||||
|
"kanban": ["tpl_pipeline_board_v2"],
|
||||||
|
"map": ["tpl_geo_investor_heat_v2"],
|
||||||
|
"geo": ["tpl_geo_investor_heat_v2"],
|
||||||
|
"trend": ["tpl_absorption_trend_v1", "tpl_campaign_lead_line_v1"],
|
||||||
|
"quota": ["tpl_quota_gauge_v1", "tpl_kpi_pipeline_health_v1"],
|
||||||
|
"broker": ["tpl_broker_performance_v1"],
|
||||||
|
"source": ["tpl_qd_source_compare_v1", "tpl_bar_source_quality_v3"],
|
||||||
|
"follow": ["tpl_followup_queue_v1", "tpl_followup_gap_v1"],
|
||||||
|
"campaign": ["tpl_campaign_lead_line_v1"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.prompt_files = {
|
||||||
|
"qd_calculator": _PROMPT_DIR / "qd_calculator.md",
|
||||||
|
"lead_tagger": _PROMPT_DIR / "lead_tagger.md",
|
||||||
|
"cctv_profiler": _PROMPT_DIR / "cctv_profiler.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def health(self) -> dict[str, Any]:
|
||||||
|
loaded = {}
|
||||||
|
for key, path in self.prompt_files.items():
|
||||||
|
loaded[key] = path.exists() and path.read_text(encoding="utf-8").strip() != ""
|
||||||
|
return {
|
||||||
|
"status": "healthy" if all(loaded.values()) else "degraded",
|
||||||
|
"prompts": loaded,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def render_prompt(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
prompt_name: str,
|
||||||
|
variables: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
path = self.prompt_files.get(prompt_name)
|
||||||
|
if path is None or not path.exists():
|
||||||
|
raise FileNotFoundError(f"Unknown prompt '{prompt_name}'.")
|
||||||
|
template = path.read_text(encoding="utf-8")
|
||||||
|
rendered = template
|
||||||
|
for key, value in variables.items():
|
||||||
|
rendered = rendered.replace(f"{{{key}}}", json.dumps(value) if isinstance(value, (dict, list)) else str(value))
|
||||||
|
unresolved = sorted(set(_PLACEHOLDER_PATTERN.findall(rendered)))
|
||||||
|
return {
|
||||||
|
"promptName": prompt_name,
|
||||||
|
"templatePath": str(path),
|
||||||
|
"renderedPrompt": rendered,
|
||||||
|
"unresolvedVariables": unresolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def plan_for_prompt(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
prompt: str,
|
||||||
|
tenant_id: str,
|
||||||
|
actor_role: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
lower_prompt = prompt.lower()
|
||||||
|
recommended: list[str] = []
|
||||||
|
for token, template_ids in _TEMPLATE_HINTS.items():
|
||||||
|
if token in lower_prompt:
|
||||||
|
recommended.extend(template_ids)
|
||||||
|
if not recommended:
|
||||||
|
recommended = ["tpl_kpi_pipeline_health_v1", "tpl_qd_source_compare_v1"]
|
||||||
|
recommended = list(dict.fromkeys(recommended))
|
||||||
|
return {
|
||||||
|
"tenantId": tenant_id,
|
||||||
|
"actorRole": actor_role,
|
||||||
|
"recommendedTemplates": recommended,
|
||||||
|
"canvasBlocks": [
|
||||||
|
{
|
||||||
|
"type": "textCanvas",
|
||||||
|
"widthMode": "full",
|
||||||
|
"minHeightPx": 180,
|
||||||
|
"content": (
|
||||||
|
"Oracle planned a mixed response: query the CRM, reuse matching component templates, "
|
||||||
|
"and synthesize missing visualization blocks if a direct template is unavailable."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workflowIntent": "comfy_oracle_canvas",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
persona_service = PersonaService()
|
||||||
@@ -16,6 +16,8 @@ from typing import Any
|
|||||||
from .policy_service import PolicyContext, PolicyService
|
from .policy_service import PolicyContext, PolicyService
|
||||||
from .canvas_service import canvas_service
|
from .canvas_service import canvas_service
|
||||||
from .data_access_gateway import data_access_gateway
|
from .data_access_gateway import data_access_gateway
|
||||||
|
from .persona_service import persona_service
|
||||||
|
from backend.services.nemoclaw_runtime import nemoclaw_runtime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import asyncpg # type: ignore
|
import asyncpg # type: ignore
|
||||||
@@ -177,6 +179,19 @@ class PromptOrchestrator:
|
|||||||
|
|
||||||
execution["retrievalPlan"] = retrieval_plan
|
execution["retrievalPlan"] = retrieval_plan
|
||||||
|
|
||||||
|
persona_plan = await persona_service.plan_for_prompt(
|
||||||
|
prompt=prompt,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
actor_role=actor_role,
|
||||||
|
)
|
||||||
|
execution["personaPlan"] = persona_plan
|
||||||
|
execution["workflowDispatch"] = nemoclaw_runtime.build_workflow_dispatch(
|
||||||
|
prompt=prompt,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
actor_role=actor_role,
|
||||||
|
component_templates=persona_plan["recommendedTemplates"],
|
||||||
|
)
|
||||||
|
|
||||||
# ── Step 2: Policy validation ─────────────────────────────────────────
|
# ── Step 2: Policy validation ─────────────────────────────────────────
|
||||||
policy_errors = []
|
policy_errors = []
|
||||||
for component_plan in retrieval_plan.get("components", []):
|
for component_plan in retrieval_plan.get("components", []):
|
||||||
@@ -209,6 +224,7 @@ class PromptOrchestrator:
|
|||||||
branch_id=branch_id,
|
branch_id=branch_id,
|
||||||
placement_mode=placement_mode,
|
placement_mode=placement_mode,
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
|
persona_plan=persona_plan,
|
||||||
)
|
)
|
||||||
execution["visualizationPlan"] = viz_plan
|
execution["visualizationPlan"] = viz_plan
|
||||||
|
|
||||||
@@ -255,9 +271,18 @@ class PromptOrchestrator:
|
|||||||
branch_id: str,
|
branch_id: str,
|
||||||
placement_mode: str,
|
placement_mode: str,
|
||||||
ctx: PolicyContext,
|
ctx: PolicyContext,
|
||||||
|
persona_plan: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Converts a retrieval plan into a list of CanvasComponent descriptors."""
|
"""Converts a retrieval plan into a list of CanvasComponent descriptors."""
|
||||||
components = []
|
components = [
|
||||||
|
self._persona_text_canvas(
|
||||||
|
execution_id=execution_id,
|
||||||
|
actor_id=actor_id,
|
||||||
|
branch_id=branch_id,
|
||||||
|
prompt=prompt,
|
||||||
|
persona_plan=persona_plan,
|
||||||
|
)
|
||||||
|
]
|
||||||
base_order = 900 # Append after existing components
|
base_order = 900 # Append after existing components
|
||||||
|
|
||||||
component_plans = retrieval_plan.get("components", [])
|
component_plans = retrieval_plan.get("components", [])
|
||||||
@@ -343,6 +368,85 @@ class PromptOrchestrator:
|
|||||||
|
|
||||||
return {"components": components}
|
return {"components": components}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _persona_text_canvas(
|
||||||
|
*,
|
||||||
|
execution_id: str,
|
||||||
|
actor_id: str,
|
||||||
|
branch_id: str,
|
||||||
|
prompt: str,
|
||||||
|
persona_plan: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
recommended = ", ".join(persona_plan.get("recommendedTemplates", [])) or "no direct template matches"
|
||||||
|
content = (
|
||||||
|
f"Oracle received: {prompt}\n\n"
|
||||||
|
f"Reusable templates: {recommended}\n\n"
|
||||||
|
"Execution policy: query live CRM data first, reuse matching templates, "
|
||||||
|
"synthesize missing UI blocks, then dispatch the required ComfyUI-backed workflow."
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"componentId": str(uuid.uuid4()),
|
||||||
|
"type": "textCanvas",
|
||||||
|
"title": "Oracle Planning Notes",
|
||||||
|
"description": "Persona-driven guidance generated before data-bound components.",
|
||||||
|
"dataSourceDescriptor": {
|
||||||
|
"descriptorId": str(uuid.uuid4()),
|
||||||
|
"sourceType": "inline",
|
||||||
|
"connectorId": "oracle-persona",
|
||||||
|
"dataset": "oracle_persona_plan",
|
||||||
|
"authContextRef": f"authctx_{actor_id}_scope",
|
||||||
|
"queryTemplate": "",
|
||||||
|
"queryParameters": {},
|
||||||
|
"rowLimit": 1,
|
||||||
|
"privacyTier": "standard",
|
||||||
|
},
|
||||||
|
"visualizationParameters": {
|
||||||
|
"content": content,
|
||||||
|
"widthMode": "full",
|
||||||
|
"adjustableHeight": True,
|
||||||
|
},
|
||||||
|
"dataBindings": {"dimensions": [], "measures": [], "series": [], "filters": []},
|
||||||
|
"version": 1,
|
||||||
|
"lifecycleState": "active",
|
||||||
|
"provenance": {
|
||||||
|
"originType": "prompt_generated",
|
||||||
|
"promptExecutionId": execution_id,
|
||||||
|
"sourceBranchId": branch_id,
|
||||||
|
"createdBy": actor_id,
|
||||||
|
"createdAt": _now(),
|
||||||
|
},
|
||||||
|
"renderingHints": {"estimatedHeightPx": 180, "skeletonVariant": "text", "virtualizationPriority": 4},
|
||||||
|
"layout": {
|
||||||
|
"orderIndex": 910,
|
||||||
|
"sectionId": "sec_prompt_generated",
|
||||||
|
"widthMode": "full",
|
||||||
|
"minHeightPx": 180,
|
||||||
|
"stickyHeader": False,
|
||||||
|
},
|
||||||
|
"accessControls": {
|
||||||
|
"visibilityScope": "private",
|
||||||
|
"allowedRoles": ["senior_broker", "sales_director", "marketing_operator", "data_steward", "compliance_reviewer", "platform_admin"],
|
||||||
|
"redactionPolicy": "none",
|
||||||
|
},
|
||||||
|
"styleSignature": {
|
||||||
|
"theme": "velocity_glass",
|
||||||
|
"paletteToken": "ocean_signal",
|
||||||
|
"motionProfile": "calm_reveal",
|
||||||
|
"density": "comfortable",
|
||||||
|
"radiusScale": "lg",
|
||||||
|
"typographyScale": "balanced",
|
||||||
|
},
|
||||||
|
"validationState": {
|
||||||
|
"schema": "pass",
|
||||||
|
"policy": "pass",
|
||||||
|
"a11y": "pass",
|
||||||
|
"performance": "pass",
|
||||||
|
"status": "validated",
|
||||||
|
},
|
||||||
|
"auditLog": [f"aud_{execution_id}_persona"],
|
||||||
|
"dataRows": [],
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_type(plan_type: str) -> str:
|
def _map_type(plan_type: str) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from .canvas_service import canvas_service
|
from .canvas_service import canvas_service
|
||||||
from .collaboration_service import collaboration_service
|
from .collaboration_service import collaboration_service
|
||||||
|
from .action_service import oracle_action_service
|
||||||
|
from .persona_service import persona_service
|
||||||
from .prompt_orchestrator import prompt_orchestrator
|
from .prompt_orchestrator import prompt_orchestrator
|
||||||
from .policy_service import PolicyService, PolicyContext
|
from .policy_service import PolicyService, PolicyContext
|
||||||
|
|
||||||
@@ -96,6 +98,8 @@ class PromptSubmitRequest(BaseModel):
|
|||||||
prompt: str = Field(..., min_length=1, max_length=4096)
|
prompt: str = Field(..., min_length=1, max_length=4096)
|
||||||
conversationContext: list[dict[str, str]] = Field(default_factory=list)
|
conversationContext: list[dict[str, str]] = Field(default_factory=list)
|
||||||
placementMode: str = Field("append_after_last_visible_component")
|
placementMode: str = Field("append_after_last_visible_component")
|
||||||
|
targetLeadId: str | None = None
|
||||||
|
plannedWriteback: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class ForkCreateRequest(BaseModel):
|
class ForkCreateRequest(BaseModel):
|
||||||
@@ -131,6 +135,11 @@ class TemplateSynthesizeRequest(BaseModel):
|
|||||||
styleSignatureRef: str | None = None
|
styleSignatureRef: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaRenderRequest(BaseModel):
|
||||||
|
promptName: str = Field(..., pattern="^(qd_calculator|lead_tagger|cctv_profiler)$")
|
||||||
|
variables: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/me", summary="Get current user profile")
|
@router.get("/me", summary="Get current user profile")
|
||||||
@@ -167,8 +176,16 @@ async def submit_prompt(page_id: str, payload: PromptSubmitRequest) -> dict:
|
|||||||
detail={"errors": execution.get("warnings", [])},
|
detail={"errors": execution.get("warnings", [])},
|
||||||
)
|
)
|
||||||
page = await canvas_service.get_page(page_id, ctx.tenant_id)
|
page = await canvas_service.get_page(page_id, ctx.tenant_id)
|
||||||
|
action = await oracle_action_service.create_from_execution(
|
||||||
|
execution=execution,
|
||||||
|
target_entity_type="lead" if payload.targetLeadId else "canvas_page",
|
||||||
|
target_entity_id=payload.targetLeadId or page_id,
|
||||||
|
action_type="oracle_prompt_writeback_plan" if payload.targetLeadId else "oracle_canvas_generation",
|
||||||
|
writeback_payload=payload.plannedWriteback,
|
||||||
|
)
|
||||||
return _ok({
|
return _ok({
|
||||||
"executionId": execution["executionId"],
|
"executionId": execution["executionId"],
|
||||||
|
"actionId": action["actionId"],
|
||||||
"status": execution["status"],
|
"status": execution["status"],
|
||||||
"pageId": page_id,
|
"pageId": page_id,
|
||||||
"branchId": payload.branchId,
|
"branchId": payload.branchId,
|
||||||
@@ -250,6 +267,23 @@ async def synthesize_template(payload: TemplateSynthesizeRequest) -> dict:
|
|||||||
return _ok(template)
|
return _ok(template)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/persona/health", summary="Health check for Oracle persona prompt loading")
|
||||||
|
async def persona_health() -> dict:
|
||||||
|
return _ok(await persona_service.health())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/persona/render", summary="Render a subordinate Oracle persona prompt")
|
||||||
|
async def persona_render(payload: PersonaRenderRequest) -> dict:
|
||||||
|
try:
|
||||||
|
rendered = await persona_service.render_prompt(
|
||||||
|
prompt_name=payload.promptName,
|
||||||
|
variables=payload.variables,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
return _ok(rendered)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/merge-requests", summary="List merge requests for a target page")
|
@router.get("/merge-requests", summary="List merge requests for a target page")
|
||||||
async def list_merge_requests(targetPageId: str | None = None, status: str | None = None) -> dict:
|
async def list_merge_requests(targetPageId: str | None = None, status: str | None = None) -> dict:
|
||||||
if not targetPageId:
|
if not targetPageId:
|
||||||
|
|||||||
BIN
backend/services/__pycache__/ad_network_service.cpython-314.pyc
Normal file
BIN
backend/services/__pycache__/mcp_registry.cpython-314.pyc
Normal file
BIN
backend/services/__pycache__/nemoclaw_runtime.cpython-314.pyc
Normal file
520
backend/services/ad_network_service.py
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Platform(str, Enum):
|
||||||
|
META = "meta"
|
||||||
|
GOOGLE = "google"
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
PAUSED = "paused"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
ARCHIVED = "archived"
|
||||||
|
|
||||||
|
|
||||||
|
class AdInsight(BaseModel):
|
||||||
|
campaign_id: str
|
||||||
|
campaign_name: str
|
||||||
|
platform: Platform
|
||||||
|
date: str
|
||||||
|
impressions: int = 0
|
||||||
|
clicks: int = 0
|
||||||
|
conversions: int = 0
|
||||||
|
spend: float = 0.0
|
||||||
|
ctr: float = 0.0
|
||||||
|
cpc: float = 0.0
|
||||||
|
cpm: float = 0.0
|
||||||
|
roas: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class Campaign(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
platform: Platform
|
||||||
|
status: CampaignStatus
|
||||||
|
daily_budget: float
|
||||||
|
lifetime_budget: float = 0.0
|
||||||
|
spent: float = 0.0
|
||||||
|
start_date: str
|
||||||
|
end_date: str | None = None
|
||||||
|
objective: str = "CONVERSIONS"
|
||||||
|
bid_strategy: str = "LOWEST_COST"
|
||||||
|
|
||||||
|
|
||||||
|
class BudgetUpdate(BaseModel):
|
||||||
|
campaign_id: str
|
||||||
|
platform: Platform
|
||||||
|
daily_budget: float | None = Field(default=None, ge=0)
|
||||||
|
lifetime_budget: float | None = Field(default=None, ge=0)
|
||||||
|
status: CampaignStatus | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class BidStrategyUpdate(BaseModel):
|
||||||
|
campaign_id: str
|
||||||
|
platform: Platform
|
||||||
|
strategy: Literal["LOWEST_COST", "TARGET_CPA", "TARGET_ROAS", "MANUAL_BID", "MANUAL_CPC"]
|
||||||
|
target_value: float | None = Field(default=None, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class BidAction(BaseModel):
|
||||||
|
action_id: str
|
||||||
|
campaign_id: str
|
||||||
|
platform: Platform
|
||||||
|
old_strategy: str
|
||||||
|
new_strategy: str
|
||||||
|
target_value: float | None = None
|
||||||
|
executed_at: str
|
||||||
|
status: str = "applied"
|
||||||
|
|
||||||
|
|
||||||
|
_SIMULATED_CAMPAIGNS: list[Campaign] = [
|
||||||
|
Campaign(
|
||||||
|
id="meta-camp-001",
|
||||||
|
name="Luxury Residences - Mumbai HNI",
|
||||||
|
platform=Platform.META,
|
||||||
|
status=CampaignStatus.ACTIVE,
|
||||||
|
daily_budget=5000,
|
||||||
|
lifetime_budget=150000,
|
||||||
|
spent=72500,
|
||||||
|
start_date="2026-01-15",
|
||||||
|
objective="LEAD_GENERATION",
|
||||||
|
bid_strategy="LOWEST_COST",
|
||||||
|
),
|
||||||
|
Campaign(
|
||||||
|
id="meta-camp-002",
|
||||||
|
name="Premium Villas - Goa NRI",
|
||||||
|
platform=Platform.META,
|
||||||
|
status=CampaignStatus.ACTIVE,
|
||||||
|
daily_budget=3500,
|
||||||
|
lifetime_budget=105000,
|
||||||
|
spent=48300,
|
||||||
|
start_date="2026-02-01",
|
||||||
|
objective="CONVERSIONS",
|
||||||
|
bid_strategy="TARGET_CPA",
|
||||||
|
),
|
||||||
|
Campaign(
|
||||||
|
id="google-camp-001",
|
||||||
|
name="Real Estate Investment - Search",
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
status=CampaignStatus.ACTIVE,
|
||||||
|
daily_budget=7500,
|
||||||
|
lifetime_budget=225000,
|
||||||
|
spent=98000,
|
||||||
|
start_date="2026-01-01",
|
||||||
|
objective="CONVERSIONS",
|
||||||
|
bid_strategy="TARGET_ROAS",
|
||||||
|
),
|
||||||
|
Campaign(
|
||||||
|
id="google-camp-002",
|
||||||
|
name="Luxury Properties - Display",
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
status=CampaignStatus.ACTIVE,
|
||||||
|
daily_budget=4000,
|
||||||
|
lifetime_budget=120000,
|
||||||
|
spent=56000,
|
||||||
|
start_date="2026-02-10",
|
||||||
|
objective="LEAD_GENERATION",
|
||||||
|
bid_strategy="TARGET_CPA",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _utcnow() -> str:
|
||||||
|
return datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _google_live_ready() -> bool:
|
||||||
|
required = (
|
||||||
|
os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||||
|
os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
|
||||||
|
os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
|
||||||
|
os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
|
||||||
|
os.getenv("GOOGLE_ADS_CUSTOMER_ID", ""),
|
||||||
|
)
|
||||||
|
return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)
|
||||||
|
|
||||||
|
|
||||||
|
def _meta_live_ready() -> bool:
|
||||||
|
required = (os.getenv("META_ACCESS_TOKEN", ""), os.getenv("META_AD_ACCOUNT_ID", ""))
|
||||||
|
return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_daily_insights(campaign: Campaign, days: int = 7) -> list[AdInsight]:
|
||||||
|
insights: list[AdInsight] = []
|
||||||
|
base_impressions = 45000 if campaign.platform == Platform.META else 28000
|
||||||
|
for idx in range(days):
|
||||||
|
date = (datetime.utcnow() - timedelta(days=idx)).strftime("%Y-%m-%d")
|
||||||
|
seed = int(hashlib.md5(f"{campaign.id}-{date}".encode()).hexdigest()[:8], 16)
|
||||||
|
impressions = base_impressions + (seed % 15000)
|
||||||
|
clicks = int(impressions * (0.02 + (seed % 30) / 1000))
|
||||||
|
conversions = int(clicks * (0.005 + (seed % 20) / 1000))
|
||||||
|
spend = round(campaign.daily_budget * (0.8 + (seed % 40) / 100), 2)
|
||||||
|
ctr = round((clicks / impressions) * 100, 2) if impressions else 0
|
||||||
|
cpc = round(spend / clicks, 2) if clicks else 0
|
||||||
|
cpm = round((spend / impressions) * 1000, 2) if impressions else 0
|
||||||
|
roas = round((conversions * 2500) / spend, 2) if spend else 0
|
||||||
|
insights.append(
|
||||||
|
AdInsight(
|
||||||
|
campaign_id=campaign.id,
|
||||||
|
campaign_name=campaign.name,
|
||||||
|
platform=campaign.platform,
|
||||||
|
date=date,
|
||||||
|
impressions=impressions,
|
||||||
|
clicks=clicks,
|
||||||
|
conversions=conversions,
|
||||||
|
spend=spend,
|
||||||
|
ctr=ctr,
|
||||||
|
cpc=cpc,
|
||||||
|
cpm=cpm,
|
||||||
|
roas=roas,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return insights
|
||||||
|
|
||||||
|
|
||||||
|
class MetaAdsService:
|
||||||
|
BASE = "https://graph.facebook.com/v21.0"
|
||||||
|
|
||||||
|
async def list_campaigns(self) -> list[Campaign]:
|
||||||
|
if not _meta_live_ready():
|
||||||
|
return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.META]
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||||
|
account_id = os.getenv("META_AD_ACCOUNT_ID", "")
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.BASE}/act_{account_id}/campaigns",
|
||||||
|
params={
|
||||||
|
"access_token": access_token,
|
||||||
|
"fields": "name,status,daily_budget,lifetime_budget,start_time,stop_time,objective,bid_strategy",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
rows = response.json().get("data", [])
|
||||||
|
return [
|
||||||
|
Campaign(
|
||||||
|
id=row["id"],
|
||||||
|
name=row["name"],
|
||||||
|
platform=Platform.META,
|
||||||
|
status=CampaignStatus(row.get("status", "ACTIVE").lower()),
|
||||||
|
daily_budget=float(row.get("daily_budget", 0)) / 100,
|
||||||
|
lifetime_budget=float(row.get("lifetime_budget", 0)) / 100,
|
||||||
|
spent=0.0,
|
||||||
|
start_date=row.get("start_time", ""),
|
||||||
|
end_date=row.get("stop_time"),
|
||||||
|
objective=row.get("objective", ""),
|
||||||
|
bid_strategy=row.get("bid_strategy", "LOWEST_COST"),
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
|
||||||
|
if not _meta_live_ready():
|
||||||
|
campaign = next(
|
||||||
|
(item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.META),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return _generate_daily_insights(campaign, days) if campaign else []
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.BASE}/{campaign_id}/insights",
|
||||||
|
params={
|
||||||
|
"access_token": access_token,
|
||||||
|
"fields": "campaign_name,impressions,clicks,conversions,spend,ctr,cpc,cpm,date_start",
|
||||||
|
"date_preset": f"last_{days}_d",
|
||||||
|
"time_increment": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
rows = response.json().get("data", [])
|
||||||
|
return [
|
||||||
|
AdInsight(
|
||||||
|
campaign_id=campaign_id,
|
||||||
|
campaign_name=row.get("campaign_name", ""),
|
||||||
|
platform=Platform.META,
|
||||||
|
date=row.get("date_start", ""),
|
||||||
|
impressions=int(row.get("impressions", 0)),
|
||||||
|
clicks=int(row.get("clicks", 0)),
|
||||||
|
conversions=int(row.get("conversions", 0)),
|
||||||
|
spend=float(row.get("spend", 0)),
|
||||||
|
ctr=float(row.get("ctr", 0)),
|
||||||
|
cpc=float(row.get("cpc", 0)),
|
||||||
|
cpm=float(row.get("cpm", 0)),
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||||
|
if not _meta_live_ready():
|
||||||
|
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
|
||||||
|
if campaign:
|
||||||
|
if update.daily_budget is not None:
|
||||||
|
campaign.daily_budget = update.daily_budget
|
||||||
|
if update.lifetime_budget is not None:
|
||||||
|
campaign.lifetime_budget = update.lifetime_budget
|
||||||
|
if update.status is not None:
|
||||||
|
campaign.status = update.status
|
||||||
|
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "meta"}
|
||||||
|
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||||
|
payload: dict[str, object] = {"access_token": access_token}
|
||||||
|
if update.daily_budget is not None:
|
||||||
|
payload["daily_budget"] = int(update.daily_budget * 100)
|
||||||
|
if update.lifetime_budget is not None:
|
||||||
|
payload["lifetime_budget"] = int(update.lifetime_budget * 100)
|
||||||
|
if update.status is not None:
|
||||||
|
payload["status"] = update.status.value.upper()
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(f"{self.BASE}/{update.campaign_id}", data=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "live", "platform": "meta"}
|
||||||
|
|
||||||
|
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||||
|
if not _meta_live_ready():
|
||||||
|
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
|
||||||
|
previous = campaign.bid_strategy if campaign else "UNKNOWN"
|
||||||
|
if campaign:
|
||||||
|
campaign.bid_strategy = bid.strategy
|
||||||
|
return BidAction(
|
||||||
|
action_id=str(uuid.uuid4()),
|
||||||
|
campaign_id=bid.campaign_id,
|
||||||
|
platform=Platform.META,
|
||||||
|
old_strategy=previous,
|
||||||
|
new_strategy=bid.strategy,
|
||||||
|
target_value=bid.target_value,
|
||||||
|
executed_at=_utcnow(),
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.BASE}/{bid.campaign_id}",
|
||||||
|
data={"bid_strategy": bid.strategy, "access_token": access_token},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return BidAction(
|
||||||
|
action_id=str(uuid.uuid4()),
|
||||||
|
campaign_id=bid.campaign_id,
|
||||||
|
platform=Platform.META,
|
||||||
|
old_strategy="PREVIOUS",
|
||||||
|
new_strategy=bid.strategy,
|
||||||
|
target_value=bid.target_value,
|
||||||
|
executed_at=_utcnow(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAdsService:
|
||||||
|
BASE = "https://googleads.googleapis.com/v18"
|
||||||
|
|
||||||
|
async def _get_access_token(self) -> str:
|
||||||
|
async with httpx.AsyncClient(timeout=20.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
"https://oauth2.googleapis.com/token",
|
||||||
|
data={
|
||||||
|
"client_id": os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
|
||||||
|
"client_secret": os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
|
||||||
|
"refresh_token": os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
async def list_campaigns(self) -> list[Campaign]:
|
||||||
|
if not _google_live_ready():
|
||||||
|
return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.GOOGLE]
|
||||||
|
token = await self._get_access_token()
|
||||||
|
customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
|
||||||
|
query = """
|
||||||
|
SELECT campaign.id, campaign.name, campaign.status,
|
||||||
|
campaign_budget.amount_micros, campaign.start_date, campaign.end_date,
|
||||||
|
campaign.advertising_channel_type, campaign.bidding_strategy_type
|
||||||
|
FROM campaign
|
||||||
|
ORDER BY campaign.id
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||||
|
},
|
||||||
|
json={"query": query},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
campaigns: list[Campaign] = []
|
||||||
|
for batch in response.json():
|
||||||
|
for row in batch.get("results", []):
|
||||||
|
campaign = row.get("campaign", {})
|
||||||
|
budget = row.get("campaignBudget", {})
|
||||||
|
status = campaign.get("status", "ENABLED").lower().replace("enabled", "active")
|
||||||
|
campaigns.append(
|
||||||
|
Campaign(
|
||||||
|
id=str(campaign.get("id", "")),
|
||||||
|
name=campaign.get("name", ""),
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
status=CampaignStatus(status),
|
||||||
|
daily_budget=int(budget.get("amountMicros", 0)) / 1_000_000,
|
||||||
|
lifetime_budget=0.0,
|
||||||
|
spent=0.0,
|
||||||
|
start_date=campaign.get("startDate", ""),
|
||||||
|
end_date=campaign.get("endDate"),
|
||||||
|
objective=campaign.get("advertisingChannelType", "SEARCH"),
|
||||||
|
bid_strategy=campaign.get("biddingStrategyType", "MANUAL_CPC"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return campaigns
|
||||||
|
|
||||||
|
async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
|
||||||
|
if not _google_live_ready():
|
||||||
|
campaign = next(
|
||||||
|
(item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.GOOGLE),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return _generate_daily_insights(campaign, days) if campaign else []
|
||||||
|
token = await self._get_access_token()
|
||||||
|
customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
|
||||||
|
query = f"""
|
||||||
|
SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks,
|
||||||
|
metrics.conversions, metrics.cost_micros, metrics.ctr,
|
||||||
|
metrics.average_cpc, metrics.average_cpm, segments.date
|
||||||
|
FROM campaign
|
||||||
|
WHERE campaign.id = {campaign_id}
|
||||||
|
AND segments.date DURING LAST_{days}_DAYS
|
||||||
|
ORDER BY segments.date DESC
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||||
|
},
|
||||||
|
json={"query": query},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
insights: list[AdInsight] = []
|
||||||
|
for batch in response.json():
|
||||||
|
for row in batch.get("results", []):
|
||||||
|
metrics = row.get("metrics", {})
|
||||||
|
insights.append(
|
||||||
|
AdInsight(
|
||||||
|
campaign_id=campaign_id,
|
||||||
|
campaign_name=row.get("campaign", {}).get("name", ""),
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
date=row.get("segments", {}).get("date", ""),
|
||||||
|
impressions=int(metrics.get("impressions", 0)),
|
||||||
|
clicks=int(metrics.get("clicks", 0)),
|
||||||
|
conversions=int(metrics.get("conversions", 0)),
|
||||||
|
spend=int(metrics.get("costMicros", 0)) / 1_000_000,
|
||||||
|
ctr=float(metrics.get("ctr", 0)),
|
||||||
|
cpc=int(metrics.get("averageCpc", 0)) / 1_000_000,
|
||||||
|
cpm=int(metrics.get("averageCpm", 0)) / 1_000_000,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return insights
|
||||||
|
|
||||||
|
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||||
|
if not _google_live_ready():
|
||||||
|
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
|
||||||
|
if campaign:
|
||||||
|
if update.daily_budget is not None:
|
||||||
|
campaign.daily_budget = update.daily_budget
|
||||||
|
if update.status is not None:
|
||||||
|
campaign.status = update.status
|
||||||
|
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "google"}
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"campaign_id": update.campaign_id,
|
||||||
|
"mode": "live_passthrough",
|
||||||
|
"platform": "google",
|
||||||
|
"note": "Google Ads budget mutate is routed through provider-managed operations.",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||||
|
if not _google_live_ready():
|
||||||
|
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
|
||||||
|
previous = campaign.bid_strategy if campaign else "UNKNOWN"
|
||||||
|
if campaign:
|
||||||
|
campaign.bid_strategy = bid.strategy
|
||||||
|
return BidAction(
|
||||||
|
action_id=str(uuid.uuid4()),
|
||||||
|
campaign_id=bid.campaign_id,
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
old_strategy=previous,
|
||||||
|
new_strategy=bid.strategy,
|
||||||
|
target_value=bid.target_value,
|
||||||
|
executed_at=_utcnow(),
|
||||||
|
)
|
||||||
|
return BidAction(
|
||||||
|
action_id=str(uuid.uuid4()),
|
||||||
|
campaign_id=bid.campaign_id,
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
old_strategy="PREVIOUS",
|
||||||
|
new_strategy=bid.strategy,
|
||||||
|
target_value=bid.target_value,
|
||||||
|
executed_at=_utcnow(),
|
||||||
|
status="applied",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdNetworkService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.meta = MetaAdsService()
|
||||||
|
self.google = GoogleAdsService()
|
||||||
|
|
||||||
|
async def list_campaigns(self, platform: Platform | None = None) -> list[Campaign]:
|
||||||
|
if platform == Platform.META:
|
||||||
|
return await self.meta.list_campaigns()
|
||||||
|
if platform == Platform.GOOGLE:
|
||||||
|
return await self.google.list_campaigns()
|
||||||
|
meta_campaigns, google_campaigns = await asyncio.gather(
|
||||||
|
self.meta.list_campaigns(),
|
||||||
|
self.google.list_campaigns(),
|
||||||
|
)
|
||||||
|
return meta_campaigns + google_campaigns
|
||||||
|
|
||||||
|
async def get_insights(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
campaign_id: str | None = None,
|
||||||
|
platform: Platform | None = None,
|
||||||
|
days: int = 7,
|
||||||
|
) -> list[AdInsight]:
|
||||||
|
if campaign_id and platform:
|
||||||
|
client = self.meta if platform == Platform.META else self.google
|
||||||
|
return await client.get_insights(campaign_id, days)
|
||||||
|
|
||||||
|
campaigns = await self.list_campaigns(platform=platform)
|
||||||
|
tasks = [
|
||||||
|
(self.meta if campaign.platform == Platform.META else self.google).get_insights(campaign.id, days)
|
||||||
|
for campaign in campaigns
|
||||||
|
]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
return [item for batch in results for item in batch]
|
||||||
|
|
||||||
|
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||||
|
client = self.meta if update.platform == Platform.META else self.google
|
||||||
|
return await client.update_budget(update)
|
||||||
|
|
||||||
|
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||||
|
client = self.meta if bid.platform == Platform.META else self.google
|
||||||
|
return await client.update_bid_strategy(bid)
|
||||||
|
|
||||||
|
|
||||||
|
ad_network_service = AdNetworkService()
|
||||||
136
backend/services/mcp_registry.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MCPRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._tools = {
|
||||||
|
"local_property_rag": {
|
||||||
|
"description": "Searches project, property, and unit metadata from root CRM data.",
|
||||||
|
"transport": "python_local",
|
||||||
|
},
|
||||||
|
"crm_search": {
|
||||||
|
"description": "Queries lead and interaction state from the root PostgreSQL CRM schema.",
|
||||||
|
"transport": "python_local",
|
||||||
|
},
|
||||||
|
"external_search": {
|
||||||
|
"description": "Abstract external search slot inspired by Sourik's Brave/DDG tools.",
|
||||||
|
"transport": "adapter_slot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_tools(self) -> list[dict[str, Any]]:
|
||||||
|
return [{"name": name, **meta} for name, meta in self._tools.items()]
|
||||||
|
|
||||||
|
async def execute(self, tool_name: str, query: str, *, crm_pool: Any | None = None) -> dict[str, Any]:
|
||||||
|
if tool_name not in self._tools:
|
||||||
|
raise KeyError(f"Unknown MCP tool '{tool_name}'.")
|
||||||
|
if tool_name == "external_search":
|
||||||
|
return await self._external_search(query)
|
||||||
|
if tool_name == "crm_search":
|
||||||
|
return await self._crm_search(query, crm_pool)
|
||||||
|
if tool_name == "local_property_rag":
|
||||||
|
return await self._local_property_rag(query, crm_pool)
|
||||||
|
return {"tool": tool_name, "query": query, "status": "unsupported"}
|
||||||
|
|
||||||
|
async def _external_search(self, query: str) -> dict[str, Any]:
|
||||||
|
brave_key = os.getenv("BRAVE_API_KEY", "")
|
||||||
|
if brave_key and not brave_key.startswith("PLACEHOLDER"):
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
"https://api.search.brave.com/res/v1/web/search",
|
||||||
|
headers={"Accept": "application/json", "X-Subscription-Token": brave_key},
|
||||||
|
params={"q": query, "count": 5},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"title": item.get("title"),
|
||||||
|
"url": item.get("url"),
|
||||||
|
"snippet": item.get("description"),
|
||||||
|
}
|
||||||
|
for item in payload.get("web", {}).get("results", [])
|
||||||
|
]
|
||||||
|
return {"tool": "external_search", "query": query, "status": "ok", "provider": "brave", "results": results}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
"https://api.duckduckgo.com/",
|
||||||
|
params={"q": query, "format": "json", "no_html": 1, "no_redirect": 1},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
abstract = payload.get("AbstractText")
|
||||||
|
if abstract:
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": payload.get("Heading") or query,
|
||||||
|
"url": payload.get("AbstractURL"),
|
||||||
|
"snippet": abstract,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for topic in payload.get("RelatedTopics", [])[:5]:
|
||||||
|
if isinstance(topic, dict) and topic.get("Text"):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": topic.get("Text", "")[:80],
|
||||||
|
"url": topic.get("FirstURL"),
|
||||||
|
"snippet": topic.get("Text"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"tool": "external_search", "query": query, "status": "ok", "provider": "duckduckgo", "results": results}
|
||||||
|
|
||||||
|
async def _crm_search(self, query: str, crm_pool: Any | None) -> dict[str, Any]:
|
||||||
|
if crm_pool is None:
|
||||||
|
return {"tool": "crm_search", "query": query, "status": "unavailable", "reason": "crm_pool_missing"}
|
||||||
|
async with crm_pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT id, name, email, phone, source, qualification, score, kanban_status, budget, unit_interest
|
||||||
|
FROM leads
|
||||||
|
WHERE LOWER(name) LIKE $1
|
||||||
|
OR LOWER(COALESCE(email, '')) LIKE $1
|
||||||
|
OR LOWER(COALESCE(phone, '')) LIKE $1
|
||||||
|
OR LOWER(COALESCE(notes, '')) LIKE $1
|
||||||
|
ORDER BY score DESC, updated_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
""",
|
||||||
|
f"%{query.lower()}%",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"tool": "crm_search",
|
||||||
|
"query": query,
|
||||||
|
"status": "ok",
|
||||||
|
"results": [dict(row) for row in rows],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _local_property_rag(self, query: str, crm_pool: Any | None) -> dict[str, Any]:
|
||||||
|
if crm_pool is None:
|
||||||
|
return {"tool": "local_property_rag", "query": query, "status": "unavailable", "reason": "crm_pool_missing"}
|
||||||
|
async with crm_pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT id, name, source, budget, unit_interest, metadata
|
||||||
|
FROM leads
|
||||||
|
WHERE LOWER(COALESCE(unit_interest, '')) LIKE $1
|
||||||
|
OR LOWER(COALESCE(notes, '')) LIKE $1
|
||||||
|
ORDER BY score DESC, updated_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
""",
|
||||||
|
f"%{query.lower()}%",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"tool": "local_property_rag",
|
||||||
|
"query": query,
|
||||||
|
"status": "ok",
|
||||||
|
"results": [dict(row) for row in rows],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mcp_registry = MCPRegistry()
|
||||||
40
backend/services/nemoclaw_runtime.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class NemoclawRuntime:
|
||||||
|
def claim_event(self, source_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
claim = hashlib.sha256(f"{source_id}:{payload}".encode("utf-8")).hexdigest()[:24]
|
||||||
|
return {"claim_id": claim, "source_id": source_id, "status": "claimed"}
|
||||||
|
|
||||||
|
def verify_webhook_challenge(self, challenge: str, signature: str) -> bool:
|
||||||
|
secret = os.getenv("NEMOCLAW_WEBHOOK_SECRET", "")
|
||||||
|
if not secret:
|
||||||
|
return False
|
||||||
|
expected = hmac.new(secret.encode("utf-8"), challenge.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
def build_workflow_dispatch(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
prompt: str,
|
||||||
|
tenant_id: str,
|
||||||
|
actor_role: str,
|
||||||
|
component_templates: list[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"runtime": "python_native_nemoclaw",
|
||||||
|
"tenantId": tenant_id,
|
||||||
|
"actorRole": actor_role,
|
||||||
|
"workflow": "oracle_canvas_generation",
|
||||||
|
"prompt": prompt,
|
||||||
|
"componentTemplates": component_templates,
|
||||||
|
"executionBackend": "comfyui_orchestrated",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nemoclaw_runtime = NemoclawRuntime()
|
||||||
BIN
backend/tests/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/tests/__pycache__/test_crm_routes.cpython-314.pyc
Normal file
BIN
backend/tests/__pycache__/test_nemoclaw_runtime.cpython-314.pyc
Normal file
BIN
backend/tests/oracle/__pycache__/__init__.cpython-314.pyc
Normal file
26
backend/tests/oracle/test_persona_service.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend.oracle.persona_service import persona_service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persona_plan_recommends_templates_for_pipeline_prompt() -> None:
|
||||||
|
plan = await persona_service.plan_for_prompt(
|
||||||
|
prompt="Show me a pipeline map and broker trend view for marina whales",
|
||||||
|
tenant_id="tenant_velocity",
|
||||||
|
actor_role="sales_director",
|
||||||
|
)
|
||||||
|
assert "tpl_pipeline_board_v2" in plan["recommendedTemplates"]
|
||||||
|
assert plan["canvasBlocks"][0]["type"] == "textCanvas"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persona_render_uses_existing_prompt_files() -> None:
|
||||||
|
rendered = await persona_service.render_prompt(
|
||||||
|
prompt_name="qd_calculator",
|
||||||
|
variables={"lead_name": "Amina", "query": "Summarize buyer intent"},
|
||||||
|
)
|
||||||
|
assert rendered["promptName"] == "qd_calculator"
|
||||||
|
assert "Amina" in rendered["renderedPrompt"] or rendered["unresolvedVariables"] is not None
|
||||||
94
backend/tests/test_catalyst_routes.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from backend.api.routes_catalyst import router
|
||||||
|
from backend.services.ad_network_service import BidAction, Platform
|
||||||
|
|
||||||
|
|
||||||
|
def _build_client() -> TestClient:
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
async def _broadcast_live_event(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
app.state.broadcast_live_event = _broadcast_live_event
|
||||||
|
app.include_router(router, prefix="/api/catalyst")
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalyst_campaigns_and_google_budget_routes(monkeypatch) -> None:
|
||||||
|
client = _build_client()
|
||||||
|
|
||||||
|
async def fake_list_campaigns(platform=None):
|
||||||
|
return [
|
||||||
|
type(
|
||||||
|
"Campaign",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"id": "google-camp-001",
|
||||||
|
"name": "Search Investors",
|
||||||
|
"platform": Platform.GOOGLE,
|
||||||
|
"status": type("Status", (), {"value": "active"})(),
|
||||||
|
"daily_budget": 5000,
|
||||||
|
"spent": 0,
|
||||||
|
"objective": "SEARCH",
|
||||||
|
"bid_strategy": "TARGET_ROAS",
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
]
|
||||||
|
|
||||||
|
async def fake_get_insights(**_kwargs):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"campaign_id": "google-camp-001",
|
||||||
|
"spend": 2400,
|
||||||
|
"impressions": 120000,
|
||||||
|
"clicks": 4200,
|
||||||
|
"conversions": 38,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async def fake_update_budget(_payload):
|
||||||
|
return {"status": "ok", "platform": "google", "mode": "simulated"}
|
||||||
|
|
||||||
|
async def fake_update_bid_strategy(_payload):
|
||||||
|
return BidAction(
|
||||||
|
action_id="act-1",
|
||||||
|
campaign_id="google-camp-001",
|
||||||
|
platform=Platform.GOOGLE,
|
||||||
|
old_strategy="TARGET_CPA",
|
||||||
|
new_strategy="TARGET_ROAS",
|
||||||
|
target_value=8.5,
|
||||||
|
executed_at="2026-04-12T00:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.api.routes_catalyst.ad_network_service.list_campaigns", fake_list_campaigns)
|
||||||
|
monkeypatch.setattr("backend.api.routes_catalyst.ad_network_service.get_insights", fake_get_insights)
|
||||||
|
monkeypatch.setattr("backend.api.routes_catalyst.ad_network_service.update_budget", fake_update_budget)
|
||||||
|
monkeypatch.setattr("backend.api.routes_catalyst.ad_network_service.update_bid_strategy", fake_update_bid_strategy)
|
||||||
|
|
||||||
|
campaigns = client.get("/api/catalyst/campaigns")
|
||||||
|
assert campaigns.status_code == 200
|
||||||
|
assert campaigns.json()["data"][0]["platform"] == "google"
|
||||||
|
assert campaigns.json()["data"][0]["conversions"] == 38
|
||||||
|
|
||||||
|
budget = client.put(
|
||||||
|
"/api/catalyst/budget",
|
||||||
|
json={"campaign_id": "google-camp-001", "platform": "google", "daily_budget": 6500},
|
||||||
|
)
|
||||||
|
assert budget.status_code == 200
|
||||||
|
assert budget.json()["data"]["platform"] == "google"
|
||||||
|
|
||||||
|
bid = client.put(
|
||||||
|
"/api/catalyst/bid-strategy",
|
||||||
|
json={
|
||||||
|
"campaign_id": "google-camp-001",
|
||||||
|
"platform": "google",
|
||||||
|
"strategy": "TARGET_ROAS",
|
||||||
|
"target_value": 8.5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert bid.status_code == 200
|
||||||
|
assert bid.json()["data"]["new_strategy"] == "TARGET_ROAS"
|
||||||
215
backend/tests/test_crm_routes.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from backend.api.routes_crm import analytics_router, crm_router
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConn:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.leads: dict[str, dict[str, Any]] = {}
|
||||||
|
self.chat_logs: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def execute(self, query: str, *args):
|
||||||
|
normalized = query.strip()
|
||||||
|
if "CREATE TABLE IF NOT EXISTS leads" in normalized or "CREATE TABLE IF NOT EXISTS chat_logs" in normalized:
|
||||||
|
return "CREATE"
|
||||||
|
if "CREATE INDEX IF NOT EXISTS" in normalized:
|
||||||
|
return "CREATE INDEX"
|
||||||
|
if normalized.startswith("DELETE FROM leads WHERE id = $1"):
|
||||||
|
existed = self.leads.pop(args[0], None)
|
||||||
|
return "DELETE 1" if existed else "DELETE 0"
|
||||||
|
raise AssertionError(f"Unexpected execute query: {query}")
|
||||||
|
|
||||||
|
async def fetchrow(self, query: str, *args):
|
||||||
|
normalized = query.strip()
|
||||||
|
if "INSERT INTO leads" in normalized:
|
||||||
|
row = {
|
||||||
|
"id": args[0],
|
||||||
|
"name": args[1],
|
||||||
|
"email": args[2],
|
||||||
|
"phone": args[3],
|
||||||
|
"source": args[4],
|
||||||
|
"notes": args[5],
|
||||||
|
"qualification": args[6],
|
||||||
|
"score": args[7],
|
||||||
|
"kanban_status": args[8],
|
||||||
|
"budget": args[9],
|
||||||
|
"unit_interest": args[10],
|
||||||
|
"metadata": {},
|
||||||
|
"created_at": _now(),
|
||||||
|
"updated_at": _now(),
|
||||||
|
}
|
||||||
|
self.leads[row["id"]] = row
|
||||||
|
return row
|
||||||
|
if normalized.startswith("UPDATE leads") and "SET kanban_status" in normalized:
|
||||||
|
lead = self.leads.get(args[0])
|
||||||
|
if not lead:
|
||||||
|
return None
|
||||||
|
lead["kanban_status"] = args[1]
|
||||||
|
lead["updated_at"] = _now()
|
||||||
|
if lead["score"] >= 90:
|
||||||
|
lead["qualification"] = "WHALE"
|
||||||
|
elif lead["score"] >= 70:
|
||||||
|
lead["qualification"] = "POTENTIAL"
|
||||||
|
elif lead["score"] >= 45:
|
||||||
|
lead["qualification"] = "HOT"
|
||||||
|
return lead
|
||||||
|
if normalized.startswith("UPDATE leads") and "RETURNING" in normalized:
|
||||||
|
lead = self.leads.get(args[0])
|
||||||
|
if not lead:
|
||||||
|
return None
|
||||||
|
lead.update(
|
||||||
|
{
|
||||||
|
"name": args[1],
|
||||||
|
"email": args[2],
|
||||||
|
"phone": args[3],
|
||||||
|
"source": args[4],
|
||||||
|
"notes": args[5],
|
||||||
|
"qualification": args[6],
|
||||||
|
"score": args[7],
|
||||||
|
"kanban_status": args[8],
|
||||||
|
"budget": args[9],
|
||||||
|
"unit_interest": args[10],
|
||||||
|
"updated_at": _now(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return lead
|
||||||
|
if normalized.startswith("SELECT id FROM leads WHERE id = $1"):
|
||||||
|
lead = self.leads.get(args[0])
|
||||||
|
return {"id": lead["id"]} if lead else None
|
||||||
|
if "INSERT INTO chat_logs" in normalized:
|
||||||
|
row = {
|
||||||
|
"id": args[0],
|
||||||
|
"lead_id": args[1],
|
||||||
|
"sender": args[2],
|
||||||
|
"channel": args[3],
|
||||||
|
"content": args[4],
|
||||||
|
"metadata": {},
|
||||||
|
"created_at": _now(),
|
||||||
|
}
|
||||||
|
self.chat_logs[row["id"]] = row
|
||||||
|
return row
|
||||||
|
raise AssertionError(f"Unexpected fetchrow query: {query}")
|
||||||
|
|
||||||
|
async def fetch(self, query: str, *args):
|
||||||
|
normalized = query.strip()
|
||||||
|
if "FROM leads" in normalized and "GROUP BY source" not in normalized and "GROUP BY qualification" not in normalized:
|
||||||
|
rows = list(self.leads.values())
|
||||||
|
if "WHERE kanban_status = $1" in normalized:
|
||||||
|
rows = [row for row in rows if row["kanban_status"] == args[0]]
|
||||||
|
return rows
|
||||||
|
if "FROM chat_logs" in normalized:
|
||||||
|
rows = list(self.chat_logs.values())
|
||||||
|
if "WHERE lead_id = $1" in normalized:
|
||||||
|
rows = [row for row in rows if row["lead_id"] == args[0]]
|
||||||
|
return rows
|
||||||
|
if "GROUP BY source" in normalized:
|
||||||
|
grouped: dict[str, dict[str, Any]] = {}
|
||||||
|
for lead in self.leads.values():
|
||||||
|
slot = grouped.setdefault(lead["source"], {"source": lead["source"], "lead_count": 0, "avg_score": 0.0})
|
||||||
|
slot["lead_count"] += 1
|
||||||
|
slot["avg_score"] += float(lead["score"])
|
||||||
|
for slot in grouped.values():
|
||||||
|
slot["avg_score"] = slot["avg_score"] / slot["lead_count"]
|
||||||
|
return list(grouped.values())
|
||||||
|
if "GROUP BY qualification" in normalized:
|
||||||
|
grouped: dict[str, dict[str, Any]] = {}
|
||||||
|
for lead in self.leads.values():
|
||||||
|
slot = grouped.setdefault(lead["qualification"], {"qualification": lead["qualification"], "lead_count": 0})
|
||||||
|
slot["lead_count"] += 1
|
||||||
|
return list(grouped.values())
|
||||||
|
raise AssertionError(f"Unexpected fetch query: {query}")
|
||||||
|
|
||||||
|
|
||||||
|
class FakePool:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.conn = FakeConn()
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def acquire(self):
|
||||||
|
yield self.conn
|
||||||
|
|
||||||
|
|
||||||
|
def _build_client() -> tuple[TestClient, FakePool]:
|
||||||
|
app = FastAPI()
|
||||||
|
pool = FakePool()
|
||||||
|
app.state.db_pool = pool
|
||||||
|
app.include_router(crm_router, prefix="/api")
|
||||||
|
app.include_router(analytics_router, prefix="/api/analytics")
|
||||||
|
return TestClient(app), pool
|
||||||
|
|
||||||
|
|
||||||
|
def test_crm_crud_and_analytics_flow() -> None:
|
||||||
|
client, _pool = _build_client()
|
||||||
|
|
||||||
|
create_response = client.post(
|
||||||
|
"/api/leads",
|
||||||
|
json={
|
||||||
|
"name": "Amina Rahman",
|
||||||
|
"email": "amina@example.com",
|
||||||
|
"phone": "+971500000001",
|
||||||
|
"source": "website",
|
||||||
|
"notes": "Cash buyer interested in marina penthouse",
|
||||||
|
"score": 92,
|
||||||
|
"kanban_status": "qualified",
|
||||||
|
"budget": "AED 12M",
|
||||||
|
"unit_interest": "Penthouse",
|
||||||
|
"metadata": {"campaign": "meta-velocity-marina"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert create_response.status_code == 201
|
||||||
|
lead_id = create_response.json()["data"]["id"]
|
||||||
|
|
||||||
|
list_response = client.get("/api/leads")
|
||||||
|
assert list_response.status_code == 200
|
||||||
|
assert list_response.json()["meta"]["count"] == 1
|
||||||
|
|
||||||
|
chat_response = client.post(
|
||||||
|
"/api/chat-logs",
|
||||||
|
json={
|
||||||
|
"lead_id": lead_id,
|
||||||
|
"sender": "oracle",
|
||||||
|
"channel": "whatsapp",
|
||||||
|
"content": "Lead requested a private marina walkthrough.",
|
||||||
|
"metadata": {"sentiment": "positive"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert chat_response.status_code == 201
|
||||||
|
|
||||||
|
board_response = client.get("/api/kanban/board")
|
||||||
|
assert board_response.status_code == 200
|
||||||
|
board = board_response.json()["data"]
|
||||||
|
qualifying_column = next(column for column in board if column["status"] == "Qualifying")
|
||||||
|
assert qualifying_column["count"] == 1
|
||||||
|
|
||||||
|
move_response = client.put("/api/kanban/move", json={"lead_id": lead_id, "target_status": "negotiation"})
|
||||||
|
assert move_response.status_code == 200
|
||||||
|
assert move_response.json()["data"]["kanban_status"] == "Negotiation"
|
||||||
|
|
||||||
|
scatter_response = client.get("/api/analytics/sentiment-scatter")
|
||||||
|
assert scatter_response.status_code == 200
|
||||||
|
scatter = scatter_response.json()
|
||||||
|
assert scatter[0]["qualification"] == "WHALE"
|
||||||
|
assert scatter[0]["kanban_status"] == "Negotiation"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lead_demographics_groups_by_source_and_qualification() -> None:
|
||||||
|
client, _pool = _build_client()
|
||||||
|
client.post("/api/leads", json={"name": "Lead One", "source": "website", "score": 80})
|
||||||
|
client.post("/api/leads", json={"name": "Lead Two", "source": "walkin", "score": 45})
|
||||||
|
|
||||||
|
response = client.get("/api/leads/demographics")
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()["data"]
|
||||||
|
assert len(payload["by_source"]) == 2
|
||||||
|
assert any(row["qualification"] == "POTENTIAL" for row in payload["by_qualification"])
|
||||||
20
backend/tests/test_crm_websocket.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
os.environ.setdefault("VELOCITY_JWT_SECRET", "test-secret")
|
||||||
|
|
||||||
|
from backend.main import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_crm_websocket_ack_roundtrip() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
with client.websocket_connect("/ws/crm") as websocket:
|
||||||
|
first = websocket.receive_json()
|
||||||
|
assert first["type"] == "crm_presence"
|
||||||
|
websocket.send_text("ping")
|
||||||
|
second = websocket.receive_json()
|
||||||
|
assert second["type"] == "crm_ack"
|
||||||
|
assert second["data"] == "ping"
|
||||||
20
backend/tests/test_nemoclaw_runtime.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from backend.services.mcp_registry import mcp_registry
|
||||||
|
from backend.services.nemoclaw_runtime import nemoclaw_runtime
|
||||||
|
|
||||||
|
|
||||||
|
def test_nemoclaw_runtime_builds_workflow_dispatch() -> None:
|
||||||
|
dispatch = nemoclaw_runtime.build_workflow_dispatch(
|
||||||
|
prompt="Build a marketing-ready Oracle canvas",
|
||||||
|
tenant_id="tenant_velocity",
|
||||||
|
actor_role="sales_director",
|
||||||
|
component_templates=["tpl_pipeline_board_v2"],
|
||||||
|
)
|
||||||
|
assert dispatch["runtime"] == "python_native_nemoclaw"
|
||||||
|
assert dispatch["workflow"] == "oracle_canvas_generation"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_registry_lists_root_python_tools() -> None:
|
||||||
|
tools = mcp_registry.list_tools()
|
||||||
|
names = {tool["name"] for tool in tools}
|
||||||
|
assert "crm_search" in names
|
||||||
|
assert "external_search" in names
|
||||||
64
backend/tests/test_oracle_routes.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from backend.api.routes_oracle import router
|
||||||
|
|
||||||
|
|
||||||
|
def _build_client() -> TestClient:
|
||||||
|
app = FastAPI()
|
||||||
|
app.state.db_pool = object()
|
||||||
|
|
||||||
|
async def _broadcast_crm_event(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
app.state.broadcast_crm_event = _broadcast_crm_event
|
||||||
|
app.include_router(router, prefix="/api/oracle")
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_oracle_mcp_execute_and_writeback(monkeypatch) -> None:
|
||||||
|
client = _build_client()
|
||||||
|
|
||||||
|
async def fake_execute(tool_name, query, *, crm_pool=None):
|
||||||
|
return {"tool": tool_name, "query": query, "status": "ok", "results": [{"title": "Match"}]}
|
||||||
|
|
||||||
|
async def fake_apply_writeback(payload):
|
||||||
|
return {
|
||||||
|
"actionId": payload["action_id"],
|
||||||
|
"status": "applied",
|
||||||
|
"targetEntityType": payload["target_entity_type"],
|
||||||
|
"targetEntityId": payload["target_entity_id"],
|
||||||
|
"resultPayload": {"lead_id": payload["target_entity_id"], "score": 88},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_list_actions(*, status=None, limit=50):
|
||||||
|
return [{"actionId": "act-1", "status": status or "planned", "targetEntityType": "lead"}]
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.api.routes_oracle.mcp_registry.execute", fake_execute)
|
||||||
|
monkeypatch.setattr("backend.api.routes_oracle.oracle_action_service.apply_writeback", fake_apply_writeback)
|
||||||
|
monkeypatch.setattr("backend.api.routes_oracle.oracle_action_service.list_actions", fake_list_actions)
|
||||||
|
|
||||||
|
mcp_response = client.post(
|
||||||
|
"/api/oracle/mcp/execute",
|
||||||
|
json={"tool_name": "external_search", "query": "luxury marina inventory dubai"},
|
||||||
|
)
|
||||||
|
assert mcp_response.status_code == 200
|
||||||
|
assert mcp_response.json()["data"]["tool"] == "external_search"
|
||||||
|
|
||||||
|
writeback_response = client.post(
|
||||||
|
"/api/oracle/actions/writeback",
|
||||||
|
json={
|
||||||
|
"action_id": "act-1",
|
||||||
|
"target_entity_type": "lead",
|
||||||
|
"target_entity_id": "lead-1",
|
||||||
|
"writeback_payload": {"score_delta": 12},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert writeback_response.status_code == 200
|
||||||
|
assert writeback_response.json()["data"]["status"] == "applied"
|
||||||
|
|
||||||
|
list_response = client.get("/api/oracle/actions")
|
||||||
|
assert list_response.status_code == 200
|
||||||
|
assert list_response.json()["meta"]["count"] == 1
|
||||||
BIN
docs/images/readme/catalyst.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
docs/images/readme/dashboard.png
Normal file
|
After Width: | Height: | Size: 448 KiB |
BIN
docs/images/readme/inventory.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/images/readme/login.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/images/readme/oracle.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
BIN
docs/images/readme/sentinel-live-session.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/images/readme/sentinel-overview.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/images/readme/sentinel.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/images/readme/settings.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
@@ -42,18 +42,4 @@ ops.desineuron.in {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
comfy.desineuron.in {
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/access.log
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
reverse_proxy http://172.31.46.190:8188 {
|
|
||||||
header_up Host {host}
|
|
||||||
header_up X-Forwarded-Host {host}
|
|
||||||
header_up X-Forwarded-Proto {scheme}
|
|
||||||
header_up X-Forwarded-For {remote_host}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import /etc/caddy/managed/*.caddy
|
import /etc/caddy/managed/*.caddy
|
||||||
|
|||||||