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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user