2026-09-12 08:57:29 +08:00
package onboarding
import (
"context"
"data-terminal-cloud/internal/outbox"
"encoding/json"
"github.com/google/uuid"
"time"
)
// Recover durable jobs after an outbox retry budget was exhausted during a
// pause. ProcessActivation owns the device lock and the no-repeat fence.
func ( s * Service ) RunMaintenance ( ctx context . Context ) {
ticker := time . NewTicker ( time . Minute )
defer ticker . Stop ()
for {
if err := s . MaintainOnce ( ctx ); err != nil && ctx . Err () == nil {
s . options . Logger . Error ( "onboarding recovery scan failed" )
}
select {
case <- ctx . Done ():
return
case <- ticker . C :
}
}
}
func ( s * Service ) MaintainOnce ( ctx context . Context ) error {
rows , err := s . pool . Query ( ctx , `SELECT j.device_id::text,j.id::text,j.binding_generation FROM device_activation_job j JOIN device d ON d.id=j.device_id JOIN device_binding b ON b.device_id=d.id AND b.unbound_at IS NULL AND b.generation=j.binding_generation WHERE d.status='registered' AND d.activation_state NOT IN ('LOST','REVOKED') AND j.binding_generation=d.binding_generation AND j.updated_at<now()-interval '3 minutes' AND (j.status IN ('NOT_STARTED','RESERVED','CONTACT_POSSIBLE') OR (j.status='REGISTERED_OBSERVED' AND j.config_ciphertext IS NULL)) ORDER BY j.updated_at LIMIT 10` )
if err != nil {
return err
}
type item struct {
DeviceID string `json:"device_id"`
ActivationID string `json:"activation_id"`
Generation int64 `json:"binding_generation"`
}
var items [] item
for rows . Next () {
var v item
if err = rows . Scan ( & v . DeviceID , & v . ActivationID , & v . Generation ); err != nil {
rows . Close ()
return err
}
items = append ( items , v )
}
rows . Close ()
if err = rows . Err (); err != nil {
return err
}
if s . options . ActivationEnabled {
for _ , v := range items {
raw , _ := json . Marshal ( v )
if err = s . ProcessActivation ( ctx , outbox . Event { ID : uuid . NewString (), Payload : raw }); err != nil {
return err
}
}
}
// Retain business attempts and idempotency tombstones for replay protection.
_ , err = s . pool . Exec ( ctx , `DELETE FROM device_handoff WHERE expires_at<now()-interval '1 day'` )
if err != nil {
return err
}
_ , err = s . pool . Exec ( ctx , `DELETE FROM device_time_challenge WHERE expires_at<now()-interval '1 day'` )
2026-10-08 10:06:44 +08:00
if err != nil {
return err
}
// Keep one day for rate limiting; delete expired OTP ciphertext with its session.
_ , err = s . pool . Exec ( ctx , `DELETE FROM customer_login_session WHERE expires_at<now()-interval '1 day'` )
if err != nil {
return err
}
_ , err = s . pool . Exec ( ctx , `DELETE FROM operator_login_session WHERE expires_at<now()-interval '1 day'` )
if err != nil {
return err
}
_ , err = s . pool . Exec ( ctx , `DELETE FROM trusted_login_device WHERE expires_at<now() OR revoked_at<now()-interval '1 day'` )
2026-09-12 08:57:29 +08:00
return err
}