A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer answers technician calls, records both call legs as separate channels, and a Ruby backend transcribes them through Whisper and files an AI summary against the caller. Squashed to a single commit for sharing. No credentials are included; secrets live outside the repo in /etc/helpdesk/env on the server or a gitignored .claude/env.local locally. See .claude/env.local.example for the shape. Start at README.md, then docs/architecture.md.
114 lines
5 KiB
SQL
114 lines
5 KiB
SQL
-- 001_init.sql — Helpdesk Postgres schema (migration from the in-memory JSON Store).
|
|
-- Idempotent / re-runnable: only CREATE ... IF NOT EXISTS (no data-changing statements here;
|
|
-- data import lives in the separate idempotent importer, db/import_json.rb).
|
|
-- This is the schema of record: the record shapes here mirror the JSON Store in lib/helpdesk/domain.rb.
|
|
-- Enums are TEXT (the app uses string values); timestamps timestamptz; IDs preserved as bigint,
|
|
-- generated via standalone sequences (PgStore.next_id -> nextval), set past max on import.
|
|
|
|
-- ---- id sequences (PgStore.next_id(kind) -> nextval("<kind>_id_seq"); ledger -> ledger_seq) ----
|
|
CREATE SEQUENCE IF NOT EXISTS org_id_seq;
|
|
CREATE SEQUENCE IF NOT EXISTS campaign_id_seq;
|
|
CREATE SEQUENCE IF NOT EXISTS person_id_seq;
|
|
CREATE SEQUENCE IF NOT EXISTS event_id_seq;
|
|
CREATE SEQUENCE IF NOT EXISTS ledger_seq;
|
|
|
|
-- ---- orgs (legacy: never written by current code; present in seed data) ----
|
|
CREATE TABLE IF NOT EXISTS orgs (
|
|
id bigint PRIMARY KEY,
|
|
name text NOT NULL,
|
|
operator text NOT NULL
|
|
);
|
|
|
|
-- ---- campaigns ----
|
|
CREATE TABLE IF NOT EXISTS campaigns (
|
|
id bigint PRIMARY KEY,
|
|
name text NOT NULL,
|
|
operator text NOT NULL
|
|
);
|
|
|
|
-- ---- dids (a number WE own) ----
|
|
CREATE TABLE IF NOT EXISTS dids (
|
|
e164 text PRIMARY KEY,
|
|
operator text NOT NULL,
|
|
campaign_id bigint REFERENCES campaigns(id)
|
|
);
|
|
|
|
-- ---- people (+ per-customer AI memory as first-class jsonb columns) ----
|
|
CREATE TABLE IF NOT EXISTS people (
|
|
id bigint PRIMARY KEY,
|
|
name text NOT NULL,
|
|
operator text NOT NULL,
|
|
email text,
|
|
context text,
|
|
position text,
|
|
org_id bigint, -- legacy, nullable, no FK (no reader/writer)
|
|
source text, -- "phone" = synced from the Pixel address book
|
|
notes_rev int DEFAULT 0,
|
|
ai_context jsonb, -- {"summary":.., "other_context":[..]} (string-keyed)
|
|
ai_context_rev int DEFAULT 0, -- optimistic-lock revision (CAS)
|
|
ai_context_pinned_sections jsonb DEFAULT '{}'::jsonb,
|
|
ai_context_edited boolean DEFAULT false,
|
|
ai_context_built_from jsonb, -- {"last_event_id":.., "model":.., "at":.., ...}
|
|
resolutions_ledger jsonb DEFAULT '[]'::jsonb,
|
|
ledger_applied_events jsonb DEFAULT '[]'::jsonb
|
|
);
|
|
|
|
-- ---- phone_numbers (e164 is the store PK; a person has many) ----
|
|
CREATE TABLE IF NOT EXISTS phone_numbers (
|
|
e164 text PRIMARY KEY,
|
|
person_id bigint REFERENCES people(id) ON DELETE CASCADE, -- delete_person deletes the numbers
|
|
raw_input text,
|
|
label text
|
|
);
|
|
|
|
-- ---- events (one row per call; scalars=columns, nested=jsonb) ----
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id bigint PRIMARY KEY,
|
|
call_id text NOT NULL UNIQUE, -- device/server-minted; hottest lookup + idempotency
|
|
person_id bigint, -- NO FK: may dangle after delete_person (verify at import)
|
|
campaign_id bigint,
|
|
did_id text,
|
|
recording_uuid text,
|
|
recording_url text,
|
|
started_at timestamptz,
|
|
answered_at timestamptz,
|
|
ended_at timestamptz,
|
|
duration_s int,
|
|
direction text, -- inbound | outbound
|
|
operator_context text,
|
|
presentation text, -- allowed | restricted | ...
|
|
state text NOT NULL, -- ringing/answered/ended/recording_uploaded/transcribed/summarised/failed/no_speech/recording_missing
|
|
notes text,
|
|
transcript text,
|
|
ai_summary text,
|
|
action_items jsonb DEFAULT '[]'::jsonb, -- [{"text":..,"kind":..,"target":..}] (string-keyed)
|
|
resolution text, -- solved | partial | unsolved | null
|
|
prior_event_id bigint, -- self-ref (repeat thread); no FK (import order)
|
|
pomoc_ticket_id text,
|
|
device_id text,
|
|
flags jsonb DEFAULT '[]'::jsonb, -- ["timeline_incomplete", ...]
|
|
muted boolean DEFAULT false,
|
|
on_hold boolean DEFAULT false,
|
|
audio_route text,
|
|
dtmf_log jsonb DEFAULT '[]'::jsonb,
|
|
disconnect_cause text,
|
|
number text,
|
|
dialed_did text,
|
|
audio_path text,
|
|
context_digest text,
|
|
caller_facts jsonb DEFAULT '[]'::jsonb, -- ["fact", ...]
|
|
ledger_deltas jsonb DEFAULT '[]'::jsonb, -- [{"entry_id":..,"question":..,"decision":..,"quote":..}]
|
|
failure_reason text,
|
|
failure_retryable boolean,
|
|
suggested_position text
|
|
);
|
|
CREATE INDEX IF NOT EXISTS events_person_id_idx ON events(person_id);
|
|
|
|
-- ---- quarantine (recording uploaded before its /incoming; reattached later) ----
|
|
CREATE TABLE IF NOT EXISTS quarantine (
|
|
recording_uuid text PRIMARY KEY,
|
|
call_id text,
|
|
url text,
|
|
audio_path text,
|
|
at timestamptz
|
|
);
|