Built the Sentinel Tab

This commit is contained in:
Sagnik
2026-04-12 02:02:58 +05:30
parent fb656d1443
commit 075ab280ad
526 changed files with 17646 additions and 70931 deletions

1
backend/db/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""backend.db package"""

Binary file not shown.

Binary file not shown.

58
backend/db/pool.py Normal file
View 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
View 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);

View 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;