1- package api
1+ package aitirepress
22
33// Phase-50 / 0033 — T3 Tire-pressure trend reasoning.
44//
@@ -64,24 +64,25 @@ import (
6464 "github.com/ev-dev-labs/teslasync/internal/ai/stream"
6565 "github.com/ev-dev-labs/teslasync/internal/ai/tools"
6666 "github.com/ev-dev-labs/teslasync/internal/ai/tools/maintenance"
67+ "github.com/ev-dev-labs/teslasync/internal/api/httpx"
6768 tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
6869 "github.com/ev-dev-labs/teslasync/internal/signal"
6970)
7071
71- // aiTirePressureTrendMaxIterations bounds the dispatcher's
72+ // maxIterations bounds the dispatcher's
7273// tool-loop. The strategy is at most query_tire_pressure_trend →
7374// answer (with optional retries). A hard ceiling of 8 is
7475// generous, matching aiCabinTemperatureImpactMaxIterations /
7576// aiCostForecastNarrationMaxIterations.
76- const aiTirePressureTrendMaxIterations = 8
77+ const maxIterations = 8
7778
78- // aiTirePressureTrendWindowDays is the trailing-window length
79+ // windowDays is the trailing-window length
7980// the production adapter projects through signal.StateReader.
8081// 30 days mirrors the slice prompt's "30-day trend" framing AND
8182// the SPA's default preset on TirePressurePage.
82- const aiTirePressureTrendWindowDays = 30
83+ const windowDays = 30
8384
84- // aiTirePressureTrendMinReadings is the minimum total
85+ // minReadings is the minimum total
8586// TpmsPressure* emission count (across all four corners) the
8687// adapter requires before it lets the narrator quote a
8788// per-tire trend. Below this threshold has_enough_data flips
@@ -90,7 +91,7 @@ const aiTirePressureTrendWindowDays = 30
9091// across a 30-day window is too sparse to fit a meaningful
9192// linear trend (TPMS re-emits on the order of once per drive,
9293// sometimes only once per week for a parked vehicle).
93- const aiTirePressureTrendMinReadings = 20
94+ const minReadings = 20
9495
9596// Pressure thresholds in Pascals (SI). Mirror the SPA's
9697// TirePressurePage (web/src/features/vehicle-systems/pages/
@@ -110,55 +111,77 @@ const (
110111// correlation hint.
111112const tireOutsideTempSignal = "OutsideTemp"
112113
113- // aiTirePressureTrendRequest is the JSON body shape this
114+ // Signal → JSON field mappings for TPMS timeline / state projection.
115+ // Field names are snake_case; the frontend camelCaseKeys transform produces
116+ // matching camelCase keys (e.g. front_left → frontLeft).
117+ var tirePressureMappings = []signal.FieldMapping {
118+ {Signal : "TpmsPressureFl" , Field : "front_left" },
119+ {Signal : "TpmsPressureFr" , Field : "front_right" },
120+ {Signal : "TpmsPressureRl" , Field : "rear_left" },
121+ {Signal : "TpmsPressureRr" , Field : "rear_right" },
122+ {Signal : "TpmsLastSeenPressureTimeFl" , Field : "last_seen_fl" },
123+ {Signal : "TpmsLastSeenPressureTimeFr" , Field : "last_seen_fr" },
124+ {Signal : "TpmsLastSeenPressureTimeRl" , Field : "last_seen_rl" },
125+ {Signal : "TpmsLastSeenPressureTimeRr" , Field : "last_seen_rr" },
126+ }
127+
128+ func writeError (w http.ResponseWriter , status int , msg string ) {
129+ httpx .WriteError (w , status , msg )
130+ }
131+
132+ func denyAllConfirm (_ context.Context , _ dispatch.ConfirmRequest ) (dispatch.ConfirmDecision , error ) {
133+ return dispatch .ConfirmDenied , nil
134+ }
135+
136+ // request is the JSON body shape this
114137// handler accepts. Mirrors the
115138// /api/v1/tire-pressure?vehicle_id= query-string contract —
116139// vehicle_id is required, no other params — kept as a JSON body
117140// so the SPA can post from the same form state the
118141// tire-pressure page already uses.
119- type aiTirePressureTrendRequest struct {
142+ type request struct {
120143 VehicleID int64 `json:"vehicle_id"`
121144}
122145
123- // AITirePressureTrendHandler is the HTTP handler for
146+ // Handler is the HTTP handler for
124147// POST /api/v1/ai/tire-pressure/trends/explain.
125148//
126149// Stateless beyond its constructor inputs; safe for concurrent
127150// use across requests. Construction is in router.go so the
128151// dispatcher's tool registry + provider registry are wired once
129152// at boot.
130- type AITirePressureTrendHandler struct {
153+ type Handler struct {
131154 registry * provider.Registry
132155 tools * tools.Registry
133156 strategy strategy.Strategy
134157 headerName string
135158 maxIters int
136159}
137160
138- // NewAITirePressureTrendHandler constructs the handler. All
161+ // NewHandler constructs the handler. All
139162// non-pointer arguments are required; the constructor panics on
140163// a nil so the wiring bug surfaces at boot, not at first
141164// request.
142- func NewAITirePressureTrendHandler (
165+ func NewHandler (
143166 registry * provider.Registry ,
144167 toolReg * tools.Registry ,
145168 strat strategy.Strategy ,
146169 headerName string ,
147- ) * AITirePressureTrendHandler {
170+ ) * Handler {
148171 switch {
149172 case registry == nil :
150- panic ("api: NewAITirePressureTrendHandler : nil provider.Registry" )
173+ panic ("aitirepress: NewHandler : nil provider.Registry" )
151174 case toolReg == nil :
152- panic ("api: NewAITirePressureTrendHandler : nil tools.Registry" )
175+ panic ("aitirepress: NewHandler : nil tools.Registry" )
153176 case strat == nil :
154- panic ("api: NewAITirePressureTrendHandler : nil strategy.Strategy" )
177+ panic ("aitirepress: NewHandler : nil strategy.Strategy" )
155178 }
156- return & AITirePressureTrendHandler {
179+ return & Handler {
157180 registry : registry ,
158181 tools : toolReg ,
159182 strategy : strat ,
160183 headerName : headerName ,
161- maxIters : aiTirePressureTrendMaxIterations ,
184+ maxIters : maxIterations ,
162185 }
163186}
164187
@@ -167,13 +190,13 @@ func NewAITirePressureTrendHandler(
167190// same parsing without constructing a full handler with stub
168191// deps. The function writes a 400 on failure and returns the
169192// (req, ok) pair so the caller can early-return.
170- func parseTirePressureTrendBody (w http.ResponseWriter , r * http.Request ) (* aiTirePressureTrendRequest , bool ) {
193+ func parseTirePressureTrendBody (w http.ResponseWriter , r * http.Request ) (* request , bool ) {
171194 if r .Body == nil {
172195 writeError (w , http .StatusBadRequest , "request body is required" )
173196 return nil , false
174197 }
175198 defer r .Body .Close ()
176- var req aiTirePressureTrendRequest
199+ var req request
177200 dec := json .NewDecoder (r .Body )
178201 dec .DisallowUnknownFields ()
179202 if err := dec .Decode (& req ); err != nil {
@@ -193,7 +216,7 @@ func parseTirePressureTrendBody(w http.ResponseWriter, r *http.Request) (*aiTire
193216// writes a structured frame onto the SSE stream (when the
194217// writer has been opened) or a plain JSON 4xx/5xx (before it
195218// has).
196- func (h * AITirePressureTrendHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
219+ func (h * Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
197220 body , ok := parseTirePressureTrendBody (w , r )
198221 if ! ok {
199222 return
@@ -262,9 +285,9 @@ func (h *AITirePressureTrendHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
262285 }
263286}
264287
265- // Compile-time assertion: AITirePressureTrendHandler satisfies
288+ // Compile-time assertion: Handler satisfies
266289// http.Handler.
267- var _ http.Handler = (* AITirePressureTrendHandler )(nil )
290+ var _ http.Handler = (* Handler )(nil )
268291
269292// ---------------------------------------------------------------------
270293// Production wiring for the tool interface declared by
@@ -292,7 +315,7 @@ type AITirePressureTrendSource struct {
292315// rather than as a nil-deref on first AI request.
293316func NewAITirePressureTrendSource (state signal.StateReader ) * AITirePressureTrendSource {
294317 if state == nil {
295- panic ("api : NewAITirePressureTrendSource: nil signal.StateReader" )
318+ panic ("aitirepress : NewAITirePressureTrendSource: nil signal.StateReader" )
296319 }
297320 return & AITirePressureTrendSource {state : state }
298321}
@@ -327,7 +350,7 @@ func (a *AITirePressureTrendSource) QueryTirePressureTrend(ctx context.Context,
327350 }
328351
329352 to := time .Now ()
330- from := to .AddDate (0 , 0 , - aiTirePressureTrendWindowDays )
353+ from := to .AddDate (0 , 0 , - windowDays )
331354
332355 // Project the 4 TPMS corners + OutsideTemp across the
333356 // 30-day window in chart mode (one row per emission, no
@@ -342,13 +365,13 @@ func (a *AITirePressureTrendSource) QueryTirePressureTrend(ctx context.Context,
342365
343366 envelope := & maintenance.TirePressureTrend {
344367 VehicleID : vehicleID ,
345- WindowDays : aiTirePressureTrendWindowDays ,
346- MinRequiredReadings : aiTirePressureTrendMinReadings ,
368+ WindowDays : windowDays ,
369+ MinRequiredReadings : minReadings ,
347370 Method : "Linear least-squares slope across the 30-day TpmsPressure* change-feed window per corner; corner status is assigned by the deterministic soft-low / normal-min / normal-max / soft-high thresholds; outside-ambient summary is the rolling 30-day average / min / max of the OutsideTemp signal." ,
348371 Assumptions : []string {
349372 "Per-corner trend is a descriptive linear slope across the recent change-feed window; it is NOT a forecast or regression model." ,
350373 "Outside ambient correlation is a heuristic: when all four corners trend down together AND the rolling average outside temperature dropped materially across the same window, seasonal contraction is the most likely deterministic driver rather than a puncture." ,
351- fmt .Sprintf ("Minimum total TpmsPressure emission count across all four corners for a meaningful narrative is %d readings; below this threshold has_enough_data is false." , aiTirePressureTrendMinReadings ),
374+ fmt .Sprintf ("Minimum total TpmsPressure emission count across all four corners for a meaningful narrative is %d readings; below this threshold has_enough_data is false." , minReadings ),
352375 },
353376 Thresholds : maintenance.TirePressureThresholds {
354377 SoftLowPa : tirePressureSoftLowPa ,
@@ -387,7 +410,7 @@ func (a *AITirePressureTrendSource) QueryTirePressureTrend(ctx context.Context,
387410 }
388411
389412 envelope .SampleSize = totalReadings
390- envelope .HasEnoughData = totalReadings >= aiTirePressureTrendMinReadings
413+ envelope .HasEnoughData = totalReadings >= minReadings
391414
392415 // Outside-temperature summary across the same window.
393416 outside := extractOutsideTempSummary (rows )
0 commit comments