Convert backend from submodule to regular directory
Some checks failed
Deploy to NAS / deploy (push) Failing after 4s
Some checks failed
Deploy to NAS / deploy (push) Failing after 4s
Remove submodule tracking; backend is now a plain directory in the repo. Also update deploy workflow: remove --recurse-submodules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
134
backend/internal/api/ingest.go
Normal file
134
backend/internal/api/ingest.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jacek/pamietnik/backend/internal/db"
|
||||
"github.com/jacek/pamietnik/backend/internal/domain"
|
||||
)
|
||||
|
||||
type trackpointInput struct {
|
||||
EventID string `json:"event_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
TripID string `json:"trip_id"`
|
||||
Timestamp string `json:"timestamp"` // RFC3339
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Source string `json:"source"`
|
||||
Note string `json:"note,omitempty"`
|
||||
AccuracyM *float64 `json:"accuracy_m,omitempty"`
|
||||
SpeedMps *float64 `json:"speed_mps,omitempty"`
|
||||
BearingDeg *float64 `json:"bearing_deg,omitempty"`
|
||||
AltitudeM *float64 `json:"altitude_m,omitempty"`
|
||||
}
|
||||
|
||||
func (t trackpointInput) toDomain() (domain.Trackpoint, error) {
|
||||
ts, err := time.Parse(time.RFC3339, t.Timestamp)
|
||||
if err != nil {
|
||||
return domain.Trackpoint{}, err
|
||||
}
|
||||
src := t.Source
|
||||
if src == "" {
|
||||
src = "gps"
|
||||
}
|
||||
return domain.Trackpoint{
|
||||
EventID: t.EventID,
|
||||
DeviceID: t.DeviceID,
|
||||
TripID: t.TripID,
|
||||
Timestamp: ts,
|
||||
Lat: t.Lat,
|
||||
Lon: t.Lon,
|
||||
Source: src,
|
||||
Note: t.Note,
|
||||
AccuracyM: t.AccuracyM,
|
||||
SpeedMps: t.SpeedMps,
|
||||
BearingDeg: t.BearingDeg,
|
||||
AltitudeM: t.AltitudeM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type batchResponse struct {
|
||||
ServerTime string `json:"server_time"`
|
||||
AcceptedIDs []string `json:"accepted_ids"`
|
||||
Rejected []db.RejectedItem `json:"rejected"`
|
||||
}
|
||||
|
||||
// HandleSingleTrackpoint handles POST /v1/trackpoints
|
||||
func HandleSingleTrackpoint(store *db.TrackpointStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var input trackpointInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON")
|
||||
return
|
||||
}
|
||||
|
||||
point, err := input.toDomain()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid timestamp: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userID := userIDFromContext(r.Context())
|
||||
accepted, rejected, err := store.UpsertBatch(r.Context(), userID, []domain.Trackpoint{point})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "database error")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, batchResponse{
|
||||
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
||||
AcceptedIDs: accepted,
|
||||
Rejected: rejected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HandleBatchTrackpoints handles POST /v1/trackpoints:batch
|
||||
func HandleBatchTrackpoints(store *db.TrackpointStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var inputs []trackpointInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&inputs); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON")
|
||||
return
|
||||
}
|
||||
if len(inputs) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "empty batch")
|
||||
return
|
||||
}
|
||||
if len(inputs) > 500 {
|
||||
writeError(w, http.StatusBadRequest, "TOO_LARGE", "batch exceeds 500 items")
|
||||
return
|
||||
}
|
||||
|
||||
points := make([]domain.Trackpoint, 0, len(inputs))
|
||||
var parseRejected []db.RejectedItem
|
||||
for _, inp := range inputs {
|
||||
p, err := inp.toDomain()
|
||||
if err != nil {
|
||||
parseRejected = append(parseRejected, db.RejectedItem{
|
||||
EventID: inp.EventID,
|
||||
Code: "INVALID_TIMESTAMP",
|
||||
Message: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
points = append(points, p)
|
||||
}
|
||||
|
||||
userID := userIDFromContext(r.Context())
|
||||
accepted, rejected, err := store.UpsertBatch(r.Context(), userID, points)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "database error")
|
||||
return
|
||||
}
|
||||
rejected = append(rejected, parseRejected...)
|
||||
|
||||
writeJSON(w, http.StatusOK, batchResponse{
|
||||
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
||||
AcceptedIDs: accepted,
|
||||
Rejected: rejected,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user