feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE memoir_chat_import (
|
||||
id uuid PRIMARY KEY, tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL REFERENCES subject_profile(id), created_by uuid NOT NULL REFERENCES user_account(id),
|
||||
title text NOT NULL, transcript_digest text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
|
||||
status text NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING','READY','FAILED'))
|
||||
);
|
||||
CREATE TABLE memoir_import_turn (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), import_id uuid NOT NULL REFERENCES memoir_chat_import(id),
|
||||
sequence_no integer NOT NULL, speaker text NOT NULL CHECK(speaker IN ('subject','agent')),
|
||||
content text NOT NULL, UNIQUE(import_id,sequence_no)
|
||||
);
|
||||
|
||||
-- One call may contain several independent events. Keep the original story
|
||||
-- origin for navigation, and carry every contributing source on revisions.
|
||||
ALTER TABLE story_draft ADD COLUMN event_index integer NOT NULL DEFAULT 0 CHECK(event_index>=0);
|
||||
ALTER TABLE story_draft ADD COLUMN event_time text NOT NULL DEFAULT '';
|
||||
ALTER TABLE story_draft ADD COLUMN import_id uuid REFERENCES memoir_chat_import(id);
|
||||
ALTER TABLE story_draft DROP CONSTRAINT story_origin_check;
|
||||
ALTER TABLE story_draft ADD CONSTRAINT story_origin_check CHECK(
|
||||
num_nonnulls(conversation_id,call_session_id,import_id)=1 AND
|
||||
((call_session_id IS NOT NULL AND call_privacy_epoch IS NOT NULL) OR (call_session_id IS NULL AND call_privacy_epoch IS NULL)));
|
||||
ALTER TABLE story_source ADD COLUMN import_turn_id uuid REFERENCES memoir_import_turn(id);
|
||||
ALTER TABLE story_source DROP CONSTRAINT story_source_origin_check;
|
||||
ALTER TABLE story_source ADD CONSTRAINT story_source_origin_check CHECK(
|
||||
num_nonnulls(conversation_turn_id,call_session_id,import_turn_id)=1 AND
|
||||
((call_session_id IS NOT NULL AND call_utterance_id IS NOT NULL AND call_source_revision IS NOT NULL) OR
|
||||
(call_session_id IS NULL AND call_utterance_id IS NULL AND call_source_revision IS NULL)));
|
||||
ALTER TABLE story_source ADD CONSTRAINT story_import_source_key UNIQUE(story_revision_id,import_turn_id);
|
||||
ALTER TABLE story_draft DROP CONSTRAINT story_draft_generation_key;
|
||||
ALTER TABLE story_draft DROP CONSTRAINT story_call_generation_key;
|
||||
ALTER TABLE story_draft ADD CONSTRAINT story_draft_generation_key UNIQUE(conversation_id,generation_version,event_index);
|
||||
ALTER TABLE story_draft ADD CONSTRAINT story_call_generation_key UNIQUE(call_session_id,generation_version,event_index);
|
||||
ALTER TABLE story_draft ADD CONSTRAINT story_import_generation_key UNIQUE(import_id,generation_version,event_index);
|
||||
|
||||
CREATE TABLE chat_daily_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL REFERENCES subject_profile(id),
|
||||
conversation_id uuid REFERENCES conversation(id),
|
||||
call_session_id uuid REFERENCES call_session(id),
|
||||
import_id uuid REFERENCES memoir_chat_import(id),
|
||||
generation_version bigint NOT NULL,
|
||||
event_index integer NOT NULL,
|
||||
event_date date NOT NULL,
|
||||
title text NOT NULL,
|
||||
content text NOT NULL,
|
||||
source_refs jsonb NOT NULL,
|
||||
model_ref text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CHECK(num_nonnulls(conversation_id,call_session_id,import_id)=1),
|
||||
UNIQUE(conversation_id,generation_version,event_index),
|
||||
UNIQUE(call_session_id,generation_version,event_index),
|
||||
UNIQUE(import_id,generation_version,event_index)
|
||||
);
|
||||
CREATE TABLE story_generation_event (
|
||||
origin_id uuid NOT NULL, generation_version bigint NOT NULL,
|
||||
event_index integer NOT NULL, story_id uuid NOT NULL REFERENCES story_draft(id),
|
||||
PRIMARY KEY(origin_id,generation_version,event_index)
|
||||
);
|
||||
CREATE TABLE memoir_document (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL REFERENCES subject_profile(id),
|
||||
version bigint NOT NULL DEFAULT 1,
|
||||
source_digest text NOT NULL,
|
||||
document jsonb NOT NULL,
|
||||
model_ref text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(subject_id,version)
|
||||
);
|
||||
CREATE INDEX memoir_document_subject_idx ON memoir_document(subject_id,version DESC);
|
||||
CREATE INDEX chat_daily_event_subject_idx ON chat_daily_event(subject_id,event_date DESC,created_at DESC);
|
||||
CREATE TABLE memoir_generation_job (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), subject_id uuid NOT NULL REFERENCES subject_profile(id),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id), actor_id uuid NOT NULL REFERENCES user_account(id),
|
||||
source_digest text NOT NULL, state text NOT NULL DEFAULT 'PENDING' CHECK(state IN ('PENDING','RUNNING','READY','FAILED')),
|
||||
error_code text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX memoir_generation_active ON memoir_generation_job(subject_id) WHERE state IN ('PENDING','RUNNING');
|
||||
CREATE TABLE memoir_illustration (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), document_id uuid NOT NULL REFERENCES memoir_document(id) ON DELETE CASCADE,
|
||||
chapter_index integer NOT NULL, content bytea NOT NULL, content_type text NOT NULL DEFAULT 'image/jpeg',
|
||||
UNIQUE(document_id,chapter_index)
|
||||
);
|
||||
|
||||
-- Every source in a merged story must remain valid, including other calls.
|
||||
CREATE OR REPLACE FUNCTION call_story_available(story uuid) RETURNS boolean LANGUAGE sql STABLE AS $$
|
||||
SELECT EXISTS(SELECT 1 FROM story_draft sd JOIN subject_profile sp ON sp.id=sd.subject_id WHERE sd.id=story AND sp.status='active'
|
||||
AND NOT EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.subject_id=sd.subject_id AND pr.request_type='DELETE')
|
||||
AND EXISTS(SELECT 1 FROM story_source ss WHERE ss.story_revision_id=sd.current_revision_id)
|
||||
AND NOT EXISTS(SELECT 1 FROM story_source ss
|
||||
LEFT JOIN call_utterance u ON u.session_id=ss.call_session_id AND u.utterance_id=ss.call_utterance_id
|
||||
LEFT JOIN call_session c ON c.id=ss.call_session_id
|
||||
LEFT JOIN conversation_turn ct ON ct.id=ss.conversation_turn_id
|
||||
LEFT JOIN conversation cv ON cv.id=ct.conversation_id
|
||||
LEFT JOIN memoir_import_turn it ON it.id=ss.import_turn_id
|
||||
LEFT JOIN memoir_chat_import ci ON ci.id=it.import_id
|
||||
WHERE ss.story_revision_id=sd.current_revision_id AND (
|
||||
(ss.call_session_id IS NOT NULL AND (c.tenant_id IS DISTINCT FROM sd.tenant_id OR c.subject_id IS DISTINCT FROM sd.subject_id
|
||||
OR NOT call_story_authorized(c.id) OR u.source_revision IS DISTINCT FROM ss.call_source_revision
|
||||
OR u.speaker<>'USER' OR u.text_content='[deleted]' OR length(trim(u.text_content))=0))
|
||||
OR (ss.conversation_turn_id IS NOT NULL AND (cv.tenant_id IS DISTINCT FROM sd.tenant_id OR cv.subject_id IS DISTINCT FROM sd.subject_id
|
||||
OR ct.speaker<>'subject' OR ct.final_text='[deleted]' OR length(trim(ct.final_text))=0
|
||||
OR NOT EXISTS(SELECT 1 FROM consent_record cr WHERE cr.subject_id=sd.subject_id AND cr.consent_type='story_extraction'
|
||||
AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now()))))
|
||||
OR (ss.import_turn_id IS NOT NULL AND (ci.tenant_id IS DISTINCT FROM sd.tenant_id OR ci.subject_id IS DISTINCT FROM sd.subject_id
|
||||
OR it.speaker<>'subject' OR it.content='[deleted]' OR length(trim(it.content))=0
|
||||
OR NOT EXISTS(SELECT 1 FROM consent_record cr WHERE cr.subject_id=sd.subject_id AND cr.consent_type='story_extraction'
|
||||
AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now()))))
|
||||
OR EXISTS(SELECT 1 FROM memory_source_suppression ms CROSS JOIN LATERAL jsonb_array_elements(ms.source_refs) r
|
||||
WHERE ms.subject_id=sd.subject_id AND (r->>'story_id'=sd.public_id::text OR
|
||||
(ss.call_session_id IS NOT NULL AND r->>'session_id'=ss.call_session_id::text
|
||||
AND (r->>'utterance_id' IS NULL OR r->>'utterance_id'=ss.call_utterance_id)) OR
|
||||
(ss.conversation_turn_id IS NOT NULL AND r->>'turn_id'=ct.public_id::text) OR
|
||||
(ss.import_turn_id IS NOT NULL AND r->>'import_id'=ci.id::text AND (r->>'turn_id' IS NULL OR r->>'turn_id'=it.id::text))))))
|
||||
)
|
||||
$$;
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user