feat: expand companion platform, device controls, and memoir workflows
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:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+116
View File
@@ -0,0 +1,116 @@
package callmemory
import (
"context"
"data-terminal-cloud/internal/identity"
"encoding/base64"
"time"
"github.com/jackc/pgx/v5"
)
type CallRecordsInput struct {
ElderID string `json:"elder_id"`
SessionID string `json:"business_session_id,omitempty"`
Cursor string `json:"cursor,omitempty"`
Limit int `json:"limit,omitempty"`
}
type recordCursor struct {
Time time.Time `json:"time"`
ID string `json:"id"`
}
func (s *Service) callRecords(ctx context.Context, tx pgx.Tx, p identity.Principal, op string, raw []byte) (any, error) {
var in CallRecordsInput
if e := StrictJSON(raw, &in); e != nil {
return nil, e
}
if in.Limit == 0 {
in.Limit = 20
}
if in.Limit < 1 || in.Limit > 100 || len(in.Cursor) > 1024 {
return nil, fault(400, "INVALID_PAGINATION")
}
a, e := ResolveAccess(ctx, tx, p, in.ElderID, false)
if e != nil {
return nil, e
}
// Transcripts have not been classified per sentence. A general read grant
// alone must not expose potentially sensitive conversation text.
if !a.Sensitive {
return nil, fault(403, "TRANSCRIPT_ACCESS_DENIED")
}
c := recordCursor{}
if in.Cursor != "" {
b, err := base64.RawURLEncoding.DecodeString(in.Cursor)
if err != nil || StrictJSON(b, &c) != nil || c.Time.IsZero() || c.ID == "" {
return nil, fault(400, "INVALID_CURSOR")
}
}
var rows pgx.Rows
if op == "calls/query" {
if in.SessionID != "" || (c.ID != "" && !validUUID(c.ID)) {
return nil, fault(400, "INVALID_CALL_ID")
}
rows, e = tx.Query(ctx, `SELECT id::text,created_at,ended_at,call_state,transcript_state,
GREATEST(0,COALESCE(EXTRACT(EPOCH FROM (ended_at-created_at))*1000,0))::bigint
FROM call_session WHERE subject_id=$1::uuid AND tenant_id=$2::uuid
AND ($3='' OR (created_at,id)<($4,NULLIF($3,'')::uuid))
ORDER BY created_at DESC,id DESC LIMIT $5`, a.Subject, a.Tenant, c.ID, c.Time, in.Limit+1)
} else {
if !validUUID(in.SessionID) {
return nil, fault(400, "INVALID_CALL_ID")
}
var exists bool
e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM call_session WHERE id=$1::uuid AND subject_id=$2::uuid AND tenant_id=$3::uuid)`, in.SessionID, a.Subject, a.Tenant).Scan(&exists)
if e != nil {
return nil, e
}
if !exists {
return nil, fault(404, "CALL_NOT_FOUND")
}
rows, e = tx.Query(ctx, `SELECT u.utterance_id,u.source_at,u.speaker,u.text_content,u.interrupted,u.playback_complete
FROM call_utterance u JOIN call_session c ON c.id=u.session_id
WHERE c.id=$1::uuid AND c.subject_id=$2::uuid AND c.tenant_id=$3::uuid
AND NOT EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.subject_id=c.subject_id AND pr.request_type='DELETE')
AND ($4='' OR (u.source_at,u.utterance_id)>($5,$4))
ORDER BY u.source_at,u.utterance_id LIMIT $6`, in.SessionID, a.Subject, a.Tenant, c.ID, c.Time, in.Limit+1)
}
if e != nil {
return nil, e
}
defer rows.Close()
items := []map[string]any{}
next := ""
last := recordCursor{}
for rows.Next() {
if len(items) == in.Limit {
next = base64.RawURLEncoding.EncodeToString(jsonBytes(last))
break
}
var id string
var at time.Time
item := map[string]any{}
if op == "calls/query" {
var ended *time.Time
var state, transcript string
var duration int64
e = rows.Scan(&id, &at, &ended, &state, &transcript, &duration)
item = map[string]any{"id": id, "created_at": at, "ended_at": ended, "status": state, "transcript_state": transcript, "duration_ms": duration, "source": "call_memory"}
} else {
var speaker, content string
var interrupted, complete bool
e = rows.Scan(&id, &at, &speaker, &content, &interrupted, &complete)
item = map[string]any{"id": id, "created_at": at, "speaker": speaker, "final_text": content, "interrupted": interrupted, "playback_complete": complete}
}
if e != nil {
return nil, e
}
last = recordCursor{at, id}
items = append(items, item)
}
if e = rows.Err(); e != nil {
return nil, e
}
return map[string]any{"items": items, "next_cursor": next}, nil
}