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
+35 -2
View File
@@ -17,6 +17,7 @@ import (
)
type ManagedDevice struct {
AccountBindingID string `json:"account_binding_id,omitempty"`
ID string `json:"id"`
BindingID string `json:"binding_id"`
HouseholdID string `json:"household_id"`
@@ -65,6 +66,8 @@ type DevicePresence struct {
Source string `json:"source"`
}
var ErrDevicePowerUnsupported = errors.New("device has not reported power control support")
func (s *Service) ListDevices(ctx context.Context, principal identity.Principal) ([]ManagedDevice, error) {
rows, err := s.pool.Query(ctx, `
SELECT d.public_id::text,b.public_id::text,h.public_id::text,sp.public_id::text,sp.preferred_name,
@@ -99,7 +102,9 @@ func (s *Service) ListDevices(ctx context.Context, principal identity.Principal)
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate managed devices: %w", err)
}
return result, nil
rows.Close()
owned, err := s.listAccountDevices(ctx, principal)
return append(result, owned...), err
}
func (s *Service) GetDevice(ctx context.Context, principal identity.Principal, devicePublicID string) (ManagedDevice, error) {
@@ -126,6 +131,16 @@ func (s *Service) PatchDeviceTwin(ctx context.Context, principal identity.Princi
if err := concurrency.RequireVersion(expectedRevision, access.desiredRevision); err != nil {
return ErrVersionConflict
}
if _, changing := desired["power"]; changing {
twin, err := readDeviceTwin(ctx, tx, access)
if err != nil {
return err
}
power, ok := twin.ReportedConfig["power"].(map[string]any)
if !ok || (power["mode"] != "active" && power["mode"] != "standby" && power["mode"] != "transition") {
return ErrDevicePowerUnsupported
}
}
desiredJSON, _ := json.Marshal(desired)
if _, err := tx.Exec(ctx, `UPDATE device_twin SET desired_config=desired_config || $1::jsonb,desired_revision=desired_revision+1,updated_at=now() WHERE device_id=$2::uuid AND tenant_id=$3::uuid`, desiredJSON, access.deviceID, access.tenantID); err != nil {
return fmt.Errorf("update desired device configuration: %w", err)
@@ -149,6 +164,24 @@ func (s *Service) GetDevicePresence(ctx context.Context, principal identity.Prin
return DevicePresence{}, err
}
result := DevicePresence{DeviceID: devicePublicID, Online: false, Status: "offline", LastSeenAt: device.LastReportedAt, Firmware: device.FirmwareVersion, Source: "database"}
// Factory-authenticated hardware reports presence through MQTT, not the
// legacy conversation telemetry Redis key. Use the same source as mini OTA.
var fleetSeen *time.Time
var fleetOnline bool
err = s.pool.QueryRow(ctx, `SELECT COALESCE(connected AND last_seen_at>now()-interval '90 seconds',false),last_seen_at
FROM fleet_presence WHERE device_id=$1`, access.deviceID).Scan(&fleetOnline, &fleetSeen)
if err == nil {
result.Online = fleetOnline
if fleetOnline {
result.Status = "online"
}
result.LastSeenAt = fleetSeen
result.Source = "mqtt"
return result, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return DevicePresence{}, err
}
if s.redis == nil {
return result, nil
}
@@ -214,7 +247,7 @@ func resolveDevice(ctx context.Context, q queryRow, principal identity.Principal
&device.Status, &device.MinimumSecurityVersion, &device.MinimumFirmwareVersion, &device.FirmwareVersion, &device.DesiredRevision, &device.ReportedRevision,
&device.LastReportedAt, &device.BoundAt, &device.RegisteredAt, &device.Version)
if errors.Is(err, pgx.ErrNoRows) {
return deviceAccess{}, ManagedDevice{}, ErrAccessDenied
return resolveAccountDevice(ctx, q, principal, publicID, capability, lock)
}
if err != nil {
return deviceAccess{}, ManagedDevice{}, fmt.Errorf("resolve device access: %w", err)