1- package api
1+ package aifeedtri
22
33// Phase-50 / 0046 — S5 Feedback queue triage.
44//
5- // ai_feedback_triage_handler .go implements the LLM-backed handler
5+ // handler .go implements the LLM-backed handler
66// at POST /api/v1/ai/feedback/triage/draft. The flow mirrors
77// ai_log_trace_summarization_handler.go (body-driven, scope-bound,
88// no persistence — one-shot read-then-propose):
@@ -68,6 +68,7 @@ package api
6868// persistence path.
6969
7070import (
71+ "bytes"
7172 "context"
7273 "encoding/json"
7374 "errors"
@@ -85,6 +86,7 @@ import (
8586 "github.com/ev-dev-labs/teslasync/internal/ai/stream"
8687 "github.com/ev-dev-labs/teslasync/internal/ai/tools"
8788 "github.com/ev-dev-labs/teslasync/internal/ai/tools/feedback"
89+ "github.com/ev-dev-labs/teslasync/internal/api/httpx"
8890 tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
8991 dbuser "github.com/ev-dev-labs/teslasync/internal/database/user"
9092)
@@ -109,6 +111,10 @@ const aiFeedbackTriageMaxBodyBytes = 16 * 1024
109111// excerpt is always a strict subset of the persisted column.
110112const aiFeedbackTriageBodyExcerptMaxChars = 4096
111113
114+ func denyAllConfirm (_ context.Context , _ dispatch.ConfirmRequest ) (dispatch.ConfirmDecision , error ) {
115+ return dispatch .ConfirmDenied , nil
116+ }
117+
112118// aiFeedbackTriageRequest is the typed body shape. feedback_id is
113119// the only required field.
114120type aiFeedbackTriageRequest struct {
@@ -117,13 +123,13 @@ type aiFeedbackTriageRequest struct {
117123 FeedbackID int64 `json:"feedback_id"`
118124}
119125
120- // AIFeedbackQueueTriageHandler is the HTTP handler for
126+ // Handler is the HTTP handler for
121127// POST /api/v1/ai/feedback/triage/draft.
122128//
123129// Stateless beyond its constructor inputs; safe for concurrent use
124130// across requests. Construction is in router.go so the dispatcher's
125131// tool registry + provider registry are wired once at boot.
126- type AIFeedbackQueueTriageHandler struct {
132+ type Handler struct {
127133 registry * provider.Registry
128134 tools * tools.Registry
129135 strategy strategy.Strategy
@@ -132,7 +138,7 @@ type AIFeedbackQueueTriageHandler struct {
132138 maxIters int
133139}
134140
135- // NewAIFeedbackQueueTriageHandler constructs the handler. All
141+ // NewHandler constructs the handler. All
136142// non-pointer arguments are required; the constructor panics on a
137143// nil so the wiring bug surfaces at boot, not at first request.
138144//
@@ -155,24 +161,24 @@ type AIFeedbackQueueTriageHandler struct {
155161// headerName: forward-auth header name; used to extract subject
156162//
157163// for audit.
158- func NewAIFeedbackQueueTriageHandler (
164+ func NewHandler (
159165 registry * provider.Registry ,
160166 toolReg * tools.Registry ,
161167 strat strategy.Strategy ,
162168 source feedback.FeedbackTriageSource ,
163169 headerName string ,
164- ) * AIFeedbackQueueTriageHandler {
170+ ) * Handler {
165171 switch {
166172 case registry == nil :
167- panic ("api: NewAIFeedbackQueueTriageHandler : nil provider.Registry" )
173+ panic ("aifeedtri: NewHandler : nil provider.Registry" )
168174 case toolReg == nil :
169- panic ("api: NewAIFeedbackQueueTriageHandler : nil tools.Registry" )
175+ panic ("aifeedtri: NewHandler : nil tools.Registry" )
170176 case strat == nil :
171- panic ("api: NewAIFeedbackQueueTriageHandler : nil strategy.Strategy" )
177+ panic ("aifeedtri: NewHandler : nil strategy.Strategy" )
172178 case source == nil :
173- panic ("api: NewAIFeedbackQueueTriageHandler : nil feedback.FeedbackTriageSource" )
179+ panic ("aifeedtri: NewHandler : nil feedback.FeedbackTriageSource" )
174180 }
175- return & AIFeedbackQueueTriageHandler {
181+ return & Handler {
176182 registry : registry ,
177183 tools : toolReg ,
178184 strategy : strat ,
@@ -189,27 +195,27 @@ func NewAIFeedbackQueueTriageHandler(
189195func parseFeedbackTriageRequest (w http.ResponseWriter , r * http.Request ) (aiFeedbackTriageRequest , bool ) {
190196 var req aiFeedbackTriageRequest
191197 if r .Body == nil {
192- writeError (w , http .StatusBadRequest , "missing body" )
198+ httpx . WriteError (w , http .StatusBadRequest , "missing body" )
193199 return req , false
194200 }
195201 defer r .Body .Close ()
196202 bodyBytes , readErr := io .ReadAll (io .LimitReader (r .Body , aiFeedbackTriageMaxBodyBytes ))
197203 if readErr != nil {
198- writeError (w , http .StatusBadRequest , fmt .Sprintf ("failed to read body: %v" , readErr ))
204+ httpx . WriteError (w , http .StatusBadRequest , fmt .Sprintf ("failed to read body: %v" , readErr ))
199205 return req , false
200206 }
201- if len (bytesTrim (bodyBytes )) == 0 {
202- writeError (w , http .StatusBadRequest , "empty body" )
207+ if len (bytes . TrimSpace (bodyBytes )) == 0 {
208+ httpx . WriteError (w , http .StatusBadRequest , "empty body" )
203209 return req , false
204210 }
205211 dec := json .NewDecoder (strings .NewReader (string (bodyBytes )))
206212 dec .DisallowUnknownFields ()
207213 if err := dec .Decode (& req ); err != nil {
208- writeError (w , http .StatusBadRequest , fmt .Sprintf ("invalid JSON body: %v" , err ))
214+ httpx . WriteError (w , http .StatusBadRequest , fmt .Sprintf ("invalid JSON body: %v" , err ))
209215 return req , false
210216 }
211217 if req .FeedbackID <= 0 {
212- writeError (w , http .StatusBadRequest , "feedback_id must be > 0" )
218+ httpx . WriteError (w , http .StatusBadRequest , "feedback_id must be > 0" )
213219 return req , false
214220 }
215221 return req , true
@@ -220,7 +226,7 @@ func parseFeedbackTriageRequest(w http.ResponseWriter, r *http.Request) (aiFeedb
220226// dispatcher's deferred WriteDone. Every error path either writes
221227// a structured frame onto the SSE stream (when the writer has been
222228// opened) or a plain JSON 4xx/5xx (before it has).
223- func (h * AIFeedbackQueueTriageHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
229+ func (h * Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
224230 // 1) Parse + validate the request body.
225231 req , ok := parseFeedbackTriageRequest (w , r )
226232 if ! ok {
@@ -233,7 +239,7 @@ func (h *AIFeedbackQueueTriageHandler) ServeHTTP(w http.ResponseWriter, r *http.
233239 // stream — emit JSON 502 so the frontend falls back gracefully.
234240 if _ , err := h .registry .For (r .Context (), feedbackqueuetriage .FeatureID ); err != nil {
235241 log .Error ().Err (err ).Msg ("ai feedback-queue-triage: provider.For failed" )
236- writeError (w , http .StatusBadGateway , "ai provider unavailable" )
242+ httpx . WriteError (w , http .StatusBadGateway , "ai provider unavailable" )
237243 return
238244 }
239245
@@ -251,7 +257,7 @@ func (h *AIFeedbackQueueTriageHandler) ServeHTTP(w http.ResponseWriter, r *http.
251257 sseW , ctx , err := stream .New (ctx , w , stream .WithFeatureID (feedbackqueuetriage .FeatureID ))
252258 if err != nil {
253259 log .Error ().Err (err ).Msg ("ai feedback-queue-triage: stream.New failed (non-flushable writer)" )
254- writeError (w , http .StatusInternalServerError , "streaming not supported" )
260+ httpx . WriteError (w , http .StatusInternalServerError , "streaming not supported" )
255261 return
256262 }
257263
@@ -314,9 +320,9 @@ func synthesizeFeedbackTriageUserMessage(feedbackID int64) string {
314320 )
315321}
316322
317- // Compile-time assertion: AIFeedbackQueueTriageHandler satisfies
323+ // Compile-time assertion: Handler satisfies
318324// http.Handler.
319- var _ http.Handler = (* AIFeedbackQueueTriageHandler )(nil )
325+ var _ http.Handler = (* Handler )(nil )
320326
321327// ---------------------------------------------------------------------
322328// Production wiring for the tool interface declared by
@@ -325,7 +331,7 @@ var _ http.Handler = (*AIFeedbackQueueTriageHandler)(nil)
325331// the log-trace-summarization slice's AILogTraceWindowSource pattern.
326332// ---------------------------------------------------------------------
327333
328- // AIFeedbackTriageSource is the production
334+ // FeedbackTriageSource is the production
329335// feedback.FeedbackTriageSource. It wraps the canonical
330336// *dbuser.UserFeedbackRepo.Get and PII-minimizes the row into a
331337// FeedbackTriageEntry: only id / created_at / category / title /
@@ -334,18 +340,18 @@ var _ http.Handler = (*AIFeedbackQueueTriageHandler)(nil)
334340// submitter_subject, submitter_ip, recent_errors, and console_tail
335341// are NOT forwarded — defence in depth on top of
336342// PolicyAlertBuilder's deny-by-default redaction.
337- type AIFeedbackTriageSource struct {
343+ type FeedbackTriageSource struct {
338344 repo * dbuser.UserFeedbackRepo
339345}
340346
341- // NewAIFeedbackTriageSource constructs the production source
347+ // NewFeedbackTriageSource constructs the production source
342348// adapter. Panics on a nil repo so the wiring bug surfaces at boot,
343349// not at first request.
344- func NewAIFeedbackTriageSource (repo * dbuser.UserFeedbackRepo ) * AIFeedbackTriageSource {
350+ func NewFeedbackTriageSource (repo * dbuser.UserFeedbackRepo ) * FeedbackTriageSource {
345351 if repo == nil {
346- panic ("api: NewAIFeedbackTriageSource : nil *dbuser.UserFeedbackRepo" )
352+ panic ("aifeedtri: NewFeedbackTriageSource : nil *dbuser.UserFeedbackRepo" )
347353 }
348- return & AIFeedbackTriageSource {repo : repo }
354+ return & FeedbackTriageSource {repo : repo }
349355}
350356
351357// LoadFeedback implements feedback.FeedbackTriageSource. Returns
@@ -357,7 +363,7 @@ func NewAIFeedbackTriageSource(repo *dbuser.UserFeedbackRepo) *AIFeedbackTriageS
357363// The body is truncated to aiFeedbackTriageBodyExcerptMaxChars to
358364// bound the prompt-token budget; the canonical body is preserved
359365// in the database column unchanged.
360- func (a * AIFeedbackTriageSource ) LoadFeedback (ctx context.Context , feedbackID int64 ) (* feedback.FeedbackTriageEntry , error ) {
366+ func (a * FeedbackTriageSource ) LoadFeedback (ctx context.Context , feedbackID int64 ) (* feedback.FeedbackTriageEntry , error ) {
361367 row , err := a .repo .Get (ctx , feedbackID )
362368 if err != nil {
363369 if errors .Is (err , dbuser .ErrFeedbackNotFound ) {
@@ -382,6 +388,6 @@ func (a *AIFeedbackTriageSource) LoadFeedback(ctx context.Context, feedbackID in
382388 }, nil
383389}
384390
385- // Compile-time assertion: AIFeedbackTriageSource satisfies
391+ // Compile-time assertion: FeedbackTriageSource satisfies
386392// feedback.FeedbackTriageSource.
387- var _ feedback.FeedbackTriageSource = (* AIFeedbackTriageSource )(nil )
393+ var _ feedback.FeedbackTriageSource = (* FeedbackTriageSource )(nil )
0 commit comments