forked from sagnik/Project_Velocity
Built the Sentinel Tab
This commit is contained in:
1
backend/db/__init__.py
Normal file
1
backend/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""backend.db package"""
|
||||
BIN
backend/db/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/db/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/db/__pycache__/pool.cpython-314.pyc
Normal file
BIN
backend/db/__pycache__/pool.cpython-314.pyc
Normal file
Binary file not shown.
58
backend/db/pool.py
Normal file
58
backend/db/pool.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
backend/db/pool.py — asyncpg Connection Pool
|
||||
|
||||
Initialises a PostgreSQL connection pool from environment variables.
|
||||
All credentials are sourced from the environment only — never hardcoded.
|
||||
|
||||
Environment variables required:
|
||||
VELOCITY_DB_HOST PostgreSQL host (default: localhost)
|
||||
VELOCITY_DB_PORT PostgreSQL port (default: 5432)
|
||||
VELOCITY_DB_NAME Database name
|
||||
VELOCITY_DB_USER Database user
|
||||
VELOCITY_DB_PASSWORD Database password (injected from AWS SSM at service start)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import asyncpg
|
||||
from fastapi import Request
|
||||
|
||||
_pool: asyncpg.Pool | None = None
|
||||
|
||||
|
||||
async def create_pool() -> asyncpg.Pool:
|
||||
"""Creates and returns the application-wide asyncpg connection pool."""
|
||||
global _pool
|
||||
_pool = await asyncpg.create_pool(
|
||||
host=os.environ.get("VELOCITY_DB_HOST", "localhost"),
|
||||
port=int(os.environ.get("VELOCITY_DB_PORT", "5432")),
|
||||
database=os.environ["VELOCITY_DB_NAME"],
|
||||
user=os.environ["VELOCITY_DB_USER"],
|
||||
password=os.environ["VELOCITY_DB_PASSWORD"],
|
||||
min_size=2,
|
||||
max_size=10,
|
||||
command_timeout=30,
|
||||
# Set app_name for easier identification in pg_stat_activity
|
||||
server_settings={"application_name": "velocity-backend"},
|
||||
)
|
||||
return _pool
|
||||
|
||||
|
||||
async def close_pool() -> None:
|
||||
"""Closes the connection pool on application shutdown."""
|
||||
global _pool
|
||||
if _pool:
|
||||
await _pool.close()
|
||||
_pool = None
|
||||
|
||||
|
||||
def get_pool(request: Request) -> asyncpg.Pool:
|
||||
"""FastAPI dependency: returns the pool stored in app.state."""
|
||||
pool: asyncpg.Pool = request.app.state.db_pool
|
||||
if pool is None:
|
||||
raise RuntimeError("Database pool is not initialised.")
|
||||
return pool
|
||||
179
backend/db/schema.sql
Normal file
179
backend/db/schema.sql
Normal file
@@ -0,0 +1,179 @@
|
||||
-- backend/db/schema.sql - Velocity PostgreSQL schema
|
||||
-- Omnichannel Intelligence Graph for The Sentinel.
|
||||
--
|
||||
-- Run via:
|
||||
-- psql -U velocity_user -d velocity_db -f schema.sql
|
||||
--
|
||||
-- Or via Alembic (preferred - see backend/alembic/).
|
||||
|
||||
-- Enable UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- ENUM TYPES
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE role_enum AS ENUM (
|
||||
'ADMIN',
|
||||
'SALES_DIRECTOR',
|
||||
'SENIOR_BROKER',
|
||||
'JUNIOR_BROKER'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE source_enum AS ENUM ('whatsapp', 'website', 'walkin');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE lead_status_enum AS ENUM ('new', 'engaged', 'qualified', 'hot', 'closed');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE qualification_enum AS ENUM ('whale', 'potential', 'tire_kicker');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE log_event_enum AS ENUM (
|
||||
'CALL_LOGGED',
|
||||
'ASSET_VIEWED',
|
||||
'SENTIMENT_SPIKE',
|
||||
'QD_UPDATED',
|
||||
'LEAD_TAGGED',
|
||||
'WS_ASSET_OPENED'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: users_and_roles (Manual RBAC backbone)
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_and_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role role_enum NOT NULL DEFAULT 'JUNIOR_BROKER',
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
last_login TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for login lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users_and_roles (email);
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: leads_intelligence (CRM core with QD scoring)
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS leads_intelligence (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
source source_enum NOT NULL,
|
||||
status lead_status_enum NOT NULL DEFAULT 'new',
|
||||
qualification qualification_enum,
|
||||
budget TEXT,
|
||||
interest TEXT,
|
||||
-- Quantum Dynamics Score: 1-100, updated in real-time by NemoClaw
|
||||
quantum_dynamics_score INTEGER CHECK (quantum_dynamics_score BETWEEN 1 AND 100),
|
||||
-- Polymorphic CRM intelligence tags e.g. ['HNI', 'NRI', 'Hot Lead']
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
assigned_to UUID REFERENCES users_and_roles(id) ON DELETE SET NULL,
|
||||
last_message TEXT,
|
||||
last_active TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Auto-update updated_at on every modification
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_leads_updated_at ON leads_intelligence;
|
||||
CREATE TRIGGER trg_leads_updated_at
|
||||
BEFORE UPDATE ON leads_intelligence
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads_intelligence (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_assigned ON leads_intelligence (assigned_to);
|
||||
-- GIN index for efficient tag array queries (e.g. WHERE 'HNI' = ANY(tags))
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_tags ON leads_intelligence USING GIN (tags);
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: velocity_vault_assets (File Tracking Engine)
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS velocity_vault_assets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
asset_name TEXT NOT NULL,
|
||||
asset_type TEXT NOT NULL, -- 'pdf', 'image', 'video'
|
||||
storage_path TEXT NOT NULL, -- relative to /opt/dlami/nvme/assets/
|
||||
-- Unique cryptographic string for every share instance
|
||||
tracking_hash VARCHAR(64) UNIQUE NOT NULL,
|
||||
lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE,
|
||||
created_by UUID REFERENCES users_and_roles(id),
|
||||
-- Array of open timestamps; one entry appended per distinct open event
|
||||
opened_at TIMESTAMPTZ[] NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_vault_hash ON velocity_vault_assets (tracking_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_vault_lead ON velocity_vault_assets (lead_id);
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: omnichannel_logs (Polymorphic event ingestion + sentimental history)
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS omnichannel_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_type log_event_enum NOT NULL,
|
||||
lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE,
|
||||
-- JSONB payload — schema varies by event_type:
|
||||
-- SENTIMENT_SPIKE: {blend_shapes, qd_before, qd_after}
|
||||
-- WS_ASSET_OPENED: {ip, user_agent, tracking_hash}
|
||||
-- QD_UPDATED: {qd_score, reasoning, confidence}
|
||||
-- LEAD_TAGGED: {tags_added, tags_removed}
|
||||
payload JSONB NOT NULL DEFAULT '{}',
|
||||
-- For MediaPipe-correlated entries: exact ms offset in the stimulus video
|
||||
video_timestamp_ms BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Composite index supporting time-series sentiment history queries
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_lead_type_time
|
||||
ON omnichannel_logs (lead_id, event_type, created_at DESC);
|
||||
|
||||
-- Partial index for fast SENTIMENT_SPIKE lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_sentiment_spikes
|
||||
ON omnichannel_logs (lead_id, created_at DESC)
|
||||
WHERE event_type = 'SENTIMENT_SPIKE';
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: consent_log (GDPR biometric consent tracking)
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS consent_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE,
|
||||
consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
-- 'granted' or 'revoked'
|
||||
action TEXT NOT NULL DEFAULT 'granted'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_consent_lead ON consent_log (lead_id, consented_at DESC);
|
||||
85
backend/db/schema_addendum.sql
Normal file
85
backend/db/schema_addendum.sql
Normal file
@@ -0,0 +1,85 @@
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- Addendum: Video Scene Maps (video_timestamp → room label mapping)
|
||||
-- Appended to schema.sql for Sprint 1 milestone.
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- TABLE: video_scene_maps
|
||||
-- Stores the timestamp-to-room mapping for each marketing video.
|
||||
-- Uploaded once per inventory item (CSV parsed and inserted by the API).
|
||||
-- Format: scene_no, start_ms, end_ms, room_type, description
|
||||
-- This allows NemoClaw to correlate a biometric reaction at T=45000ms with
|
||||
-- "Master Bedroom" for contextual QD scoring.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_scene_maps (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
video_asset_id TEXT NOT NULL, -- Matches inventory item slug / asset filename
|
||||
scene_no INTEGER NOT NULL,
|
||||
start_ms BIGINT NOT NULL,
|
||||
end_ms BIGINT NOT NULL,
|
||||
room_type TEXT NOT NULL, -- e.g. 'Living Room', 'Master Bedroom', 'Balcony'
|
||||
description TEXT, -- Optional: 'Ocean-facing balcony with pool view'
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (video_asset_id, scene_no)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_scenes_asset_range
|
||||
ON video_scene_maps (video_asset_id, start_ms, end_ms);
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: perception_sessions
|
||||
-- Tracks each PerceptionPlayer session (assigned or auto mode).
|
||||
-- Assigned Mode: lead_id is set before session starts.
|
||||
-- Auto Mode : lead_id is NULL; auto_mode_matched_at populated post hoc.
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE session_mode_enum AS ENUM ('assigned', 'auto');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS perception_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_mode session_mode_enum NOT NULL DEFAULT 'assigned',
|
||||
lead_id UUID REFERENCES leads_intelligence(id) ON DELETE SET NULL,
|
||||
video_asset_id TEXT NOT NULL,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ended_at TIMESTAMPTZ,
|
||||
final_qd_score INTEGER CHECK (final_qd_score BETWEEN 1 AND 100),
|
||||
-- For auto mode: the lead_id matched after session by face/plate recognition
|
||||
auto_mode_matched_at TIMESTAMPTZ,
|
||||
-- JSONB blob with auto-mode gathered data: face_hash, plate, vehicle_class, etc.
|
||||
auto_mode_evidence JSONB DEFAULT '{}',
|
||||
broker_user_id UUID REFERENCES users_and_roles(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_lead ON perception_sessions (lead_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_unmatched
|
||||
ON perception_sessions (started_at DESC)
|
||||
WHERE session_mode = 'auto' AND lead_id IS NULL;
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- TABLE: cctv_events
|
||||
-- Records each parking/entry visitor event from CCTV feeds.
|
||||
-- License plates, vehicle class, NemoClaw wealth indicator.
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cctv_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
zone TEXT NOT NULL, -- 'Parking Entry', 'Main Gate', 'Zone A', etc.
|
||||
license_plate TEXT, -- Raw OCR text
|
||||
vehicle_class TEXT, -- 'luxury' | 'standard' | 'unknown'
|
||||
wealth_indicator TEXT, -- 'HNI' | 'standard' | 'unknown'
|
||||
nemoclaw_tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
nemoclaw_notes TEXT,
|
||||
linked_lead_id UUID REFERENCES leads_intelligence(id) ON DELETE SET NULL,
|
||||
linked_session_id UUID REFERENCES perception_sessions(id) ON DELETE SET NULL,
|
||||
captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
raw_payload JSONB NOT NULL DEFAULT '{}' -- Full CCTV frame metadata
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cctv_plate ON cctv_events (license_plate, captured_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cctv_zone ON cctv_events (zone, captured_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cctv_unlinked
|
||||
ON cctv_events (captured_at DESC)
|
||||
WHERE linked_lead_id IS NULL;
|
||||
Reference in New Issue
Block a user