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
+43 -15
View File
@@ -22,21 +22,22 @@ const (
)
type DeviceBindingImpact struct {
DeviceID string `json:"device_id"`
BindingID string `json:"binding_id"`
Action string `json:"action"`
SourceHouseholdID string `json:"source_household_id"`
SourceSubjectID string `json:"source_subject_id"`
SourceSubjectName string `json:"source_subject_name"`
TargetHouseholdID *string `json:"target_household_id,omitempty"`
TargetSubjectID *string `json:"target_subject_id,omitempty"`
TargetSubjectName *string `json:"target_subject_name,omitempty"`
ActiveConversationID *string `json:"active_conversation_id,omitempty"`
PendingCommandCount int `json:"pending_command_count"`
ActiveFirmwareTaskCount int `json:"active_firmware_task_count"`
Allowed bool `json:"allowed"`
Blockers []string `json:"blockers"`
EvaluatedAt time.Time `json:"evaluated_at"`
DeviceID string `json:"device_id"`
BindingID string `json:"binding_id"`
Action string `json:"action"`
SourceHouseholdID string `json:"source_household_id"`
SourceSubjectID string `json:"source_subject_id"`
SourceSubjectName string `json:"source_subject_name"`
TargetHouseholdID *string `json:"target_household_id,omitempty"`
TargetSubjectID *string `json:"target_subject_id,omitempty"`
TargetSubjectName *string `json:"target_subject_name,omitempty"`
ActiveConversationID *string `json:"active_conversation_id,omitempty"`
PendingCommandCount int `json:"pending_command_count"`
ActiveFirmwareTaskCount int `json:"active_firmware_task_count"`
Allowed bool `json:"allowed"`
RequiresPhysicalConfirmation bool `json:"requires_physical_confirmation,omitempty"`
Blockers []string `json:"blockers"`
EvaluatedAt time.Time `json:"evaluated_at"`
}
type ChangeDeviceBindingInput struct {
@@ -72,6 +73,10 @@ func (s *Service) ChangeDeviceBinding(ctx context.Context, principal identity.Pr
if err != nil {
return err
}
if device.AccountBindingID != "" {
result, err = s.releaseAccountDevice(ctx, tx, principal, access, device, requestID, input)
return err
}
var bindingID string
if input.Action == DeviceBindingActionTransfer {
if err = devicebinding.RequireLegacy(ctx, tx, access.deviceID); err != nil {
@@ -146,12 +151,18 @@ func (s *Service) readDeviceBindingImpact(ctx context.Context, q queryRow, princ
if action != DeviceBindingActionUnbind && action != DeviceBindingActionTransfer {
return DeviceBindingImpact{}, ErrNotFound
}
if device.AccountBindingID != "" && action != DeviceBindingActionUnbind {
return DeviceBindingImpact{}, ErrNotFound
}
impact := DeviceBindingImpact{
DeviceID: device.ID, BindingID: device.BindingID, Action: action,
SourceHouseholdID: device.HouseholdID, SourceSubjectID: device.SubjectID, SourceSubjectName: device.SubjectName,
Blockers: make([]string, 0), EvaluatedAt: time.Now().UTC(),
}
if action == DeviceBindingActionTransfer {
if err := q.QueryRow(ctx, `SELECT enrollment_mode='physical_confirm' FROM device WHERE id=$1::uuid`, access.deviceID).Scan(&impact.RequiresPhysicalConfirmation); err != nil {
return DeviceBindingImpact{}, err
}
if targetSubjectPublicID == "" || targetSubjectPublicID == device.SubjectID {
impact.Blockers = append(impact.Blockers, "target_subject_required")
} else {
@@ -172,6 +183,13 @@ func (s *Service) readDeviceBindingImpact(ctx context.Context, q queryRow, princ
} else if !errors.Is(err, pgx.ErrNoRows) {
return DeviceBindingImpact{}, fmt.Errorf("check active device conversation: %w", err)
}
var activeCall bool
if err := q.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM call_session WHERE device_id=$1::uuid AND call_state IN ('PREPARING','STARTING','TALKING','STOPPING'))`, access.deviceID).Scan(&activeCall); err != nil {
return DeviceBindingImpact{}, err
}
if activeCall {
impact.Blockers = append(impact.Blockers, "active_call")
}
if err := q.QueryRow(ctx, `SELECT count(*) FROM device_command WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND status IN ('QUEUED','DELIVERED','ACKNOWLEDGED') AND expires_at>now()`, access.deviceID, access.tenantID).Scan(&impact.PendingCommandCount); err != nil {
return DeviceBindingImpact{}, fmt.Errorf("count pending device commands: %w", err)
}
@@ -184,6 +202,16 @@ func (s *Service) readDeviceBindingImpact(ctx context.Context, q queryRow, princ
if impact.ActiveFirmwareTaskCount > 0 {
impact.Blockers = append(impact.Blockers, "active_firmware_task")
}
if s.Fleet != nil {
var active int
if err := q.QueryRow(ctx, `SELECT count(*) FROM fleet_ota WHERE device_id=$1 AND status IN ('pending','accepted')`, access.deviceID).Scan(&active); err != nil {
return DeviceBindingImpact{}, err
}
impact.ActiveFirmwareTaskCount += active
if active > 0 {
impact.Blockers = append(impact.Blockers, "active_mqtt_ota")
}
}
impact.Allowed = len(impact.Blockers) == 0
return impact, nil
}