Skip to content

Commit 04fc5f8

Browse files
atulmguptaCopilot
andauthored
Chore/repo reorganization (#73)
* chore(blame): ignore R2.0e apperror carve 673da4e1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2a): carve internal/api/backup (first resource handler subpkg) First resource carve under Phase R2 — moves the two backup handlers out of the flat internal/api parent into a self-contained subpkg. Exercises the full carve pattern (handler types, route mounts, AppError catalog usage) on a moderately-sized surface (~14 KB across two files; backup_restore_handler.go was one of the heaviest writeAppError consumers in the parent). Why this is the right R2a starter --------------------------------- - Bounded route prefix: /api/v1/system/backup{,/stats} + /api/v1/backup/*. No leakage into other resource clusters. - Heaviest AppError consumer in internal/api by call-site count (26 writeAppError + 18 WithMessage in backup_restore_handler.go); proves the post-R2.0e apperror surface end-to-end. - Modest scope (~14 KB, 13 endpoints) makes a clean reviewable diff before tackling bigger clusters (vehicle, drives, charging). New package: internal/api/backup (Layer: handler) - handler.go Handler (former BackupHandler) — admin-style data-export endpoints. ExportData streams JSON for every table in AllowedTables; BackupStats returns DB size + row counts. AllowedTables is exported (was unexported allowedBackupTables) so the regression test in this package and any future tooling reference one canonical map. - restore_handler.go RestoreHandler (former BackupRestoreHandler) — config CRUD (List/Get/Create/Update/Delete), run management (List/Get), trigger (TriggerBackup/TriggerQuickBackup), and download/verify/preview-restore. All error paths now call apperror.Write directly (no parent writeAppError wrapper); all success paths call httpx.WriteJSON directly. - doc.go Layer: handler + the package-name-collision rationale (internal/backup is also pkg backup; imported here as `corebackup`). Internal cleanups (no behavior change) - clampConfigBounds extracted from CreateConfig + UpdateConfig (was duplicated; ~20 LOC each side). - providerConfigForRun extracted from DownloadBackup + VerifyBackup + PreviewRestore (was triplicate). - DRY consolidation visible in the diff but produces byte- identical responses to the prior implementation. Parent rewires (internal/api) - router.go Added `apibackup "internal/api/backup"` import. Replaced: NewBackupHandler(db) → apibackup.NewHandler(db) NewBackupRestoreHandler(db) → apibackup.NewRestoreHandler(db) Both route mounts unchanged at the method-call site (var names `backupHandler` / `backupRestoreHandler` preserved for minimal diff). - handlers_test.go Removed TestAllowedBackupTables (relocated to internal/api/backup/handler_test.go alongside the canonical apibackup.AllowedTables). Left a Phase R2a relocation breadcrumb. TestAllowedCommandsWhitelist stays — commands handler is unrelated and not part of R2a. Parent files deleted - internal/api/backup_handler.go (3196 bytes) - internal/api/backup_restore_handler.go (11061 bytes) Tests added - internal/api/backup/handler_test.go * TestAllowedTables_RequiredAndForbiddenEntries — pins the whitelist on both axes (required-present spread; required- absent dangerous tables like pg_shadow / tokens / api_keys). External `package backup_test` so it exercises the exported AllowedTables surface, not the package-internal var. Gates - gofmt + goimports clean - go vet ./internal/api/... + go build ./... clean - go test ./... PASS (incl. apperror_bridge_test, arch tests) - go test -race ./internal/api/backup/... PASS - golangci-lint run ./internal/api/... clean - go run ./tools/archmetrics -compare tools/archmetrics/baseline.json OK - baseline.json + baseline.md refreshed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(blame): ignore R2a backup carve 11ccd511 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2.0f): carve internal/api/apibulk (bulk-endpoint catalog + helpers) R2.0f lifts the cross-resource bulk-endpoint contract out of the flat parent internal/api/bulk_helpers.go into a dedicated subpackage. This is the bulk-API analog of R2.0e (apperror) — shared HANDLER-LAYER infrastructure that every resource subpackage in the Phase R2 wave can import directly without coupling to the parent. Subpkg surface (internal/api/apibulk): - MaxIDs const (default 500-cap) - Wire shapes: IDsBody, OpBody, FailedID, OperationResult - Sentinel decode errors: ErrBodyInvalid, ErrIDsEmpty, ErrIDsTooMany - Pure helpers: DedupeInt64s, ComputeMissingIDs - HTTP-coupled helpers: DecodeIDsRequest, DecodeOpBody, WriteBadRequest Parent bridge (internal/api/bulk_helpers.go): - TRUE const alias: MaxBulkIDs = apibulk.MaxIDs - TRUE type aliases: bulkIDsBody, bulkFailedID, bulkOperationResult, automationBulkBody — identical reflect.Type to canonicals - Var bridges: errBulkBodyInvalid, errBulkIDsEmpty, errBulkIDsTooMany - 1-line wrapper funcs: decodeBulkIDsRequest, decodeAutomationBulkBody, dedupeInt64s, computeMissingIDs, writeBulkBadRequest Side edit: - internal/api/automations_bulk_handler.go: deleted local automationBulkBody type + decodeAutomationBulkBody func (now both served by the apibulk bridge to avoid duplication). Comment breadcrumbs left in place. Tests: - internal/api/apibulk/helpers_test.go: 18 cases covering decode happy paths, dedup, sentinel-error mapping, ComputeMissingIDs non-nil invariant, WriteBadRequest 400 + flat error envelope wire-shape, sentinel message stability. - internal/api/bulk_helpers_bridge_test.go: pins const alias, reflect.Type-equality of all 4 type aliases, errors.Is pointer equality of the 3 sentinel var bridges, wrapper-delegates-to- canonical checks for DedupeInt64s + ComputeMissingIDs (incl. aliased-slice-assignable-to-canonical-slice test). Why now (in front of R2b geofence): Nine bulk handlers (alerts, automations, charging, drives, exports, geofences/bulk, push, saved_views, ...) all currently call the flat parent helpers. As R2b carves geofence into a subpackage, its bulk endpoint loses access to the parent (subpkg -> parent imports would create a cycle). Without apibulk, R2b would have to either duplicate the 150-LOC helpers into its subpkg (DRY violation, drift risk for the wire-shape contract the frontend depends on) or block on a much bigger architectural refactor. R2.0f unblocks every remaining R2 carve cleanly. ADR-009 exception: Added bulk_helpers_bridge_test.go row to .github/ARCHITECTURE.md matching the precedent set by R2.0e/apperror_bridge_test.go. The bridge test MUST live in package api to name both parent and subpkg symbols simultaneously. Verification: - gofmt -w (clean) - go vet ./... PASS - go build ./... PASS - go test ./... PASS (all ~120 pkgs) - go test -race ./internal/api/apibulk/... PASS - go test -race ./internal/api/... PASS - golangci-lint run ./internal/api/{apibulk,}/... CLEAN - tools/archmetrics -compare baseline.json OK (no regression) Wire-shape preserved: parent's flat {"error":"..."} envelope (via httpx.WriteError) is byte-identical to apibulk.WriteBadRequest; pinned by TestWriteBadRequest_400FlatShape in the apibulk test suite. Refs: Phase R2.0e (apperror catalog, 673da4e1) — same pattern. Refs: Phase R2a (backup subpkg, 11ccd511) — first resource carve, now unblocked-via-apibulk for R2b. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(blame): ignore R2.0f apibulk carve d5bab8b3 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2b): carve internal/api/geofence (CRUD + bulk handlers) Moves the /api/v1/geofences resource cluster out of the flat parent internal/api/ into a dedicated subpackage. Follows the pattern set by R2a (backup) and uses the shared infrastructure subpackages from R2.0d/R2.0e/R2.0f (apperror, apibulk, apiparams, httpx) so the subpackage has zero dependency on the parent. Carved files (3, blame preserved on 2): - internal/api/geofence_handler.go -> internal/api/geofence/handler.go (R) - internal/api/geofence_handler_test.go -> internal/api/geofence/handler_test.go (R) - internal/api/geofences_bulk_handler.go -> internal/api/geofence/bulk_handler.go (rewritten - new file) New file: - internal/api/geofence/doc.go - Layer/Why/Scope/Test-time substitution Architecture decisions (precedent for R2c/R2d/R2e): - Options pattern over exported test-only fields: NewHandler(db) for prod, NewHandler(nil, WithBulkStore(fake)) for tests. No BulkOverride field. - Audit injection via callback (AuditFunc closure bound in router.go) so the subpackage can audit bulk_delete events without importing parent. - AI handler cluster (ai_suggest_new_geofences_*) stays in parent until R2e; it has its own AISuggestGeofenceValidator that byte-equivalently mirrors validateGeofence. Wiring: - router.go: replaced `NewGeofenceHandler(db)` with `apigeo.NewHandler(db, apigeo.WithAuditFunc(...))` closure that delegates to logAuditFromRequest. Method calls unchanged (List/Create/Get/Update/Delete/BulkUpdate compatible). - bulk_handlers_phase45_test.go: 4 geofence test cases updated to `apigeo.NewHandler(nil, apigeo.WithBulkStore(store))`. fakeGeofenceBulkStore now satisfies apigeo.BulkStore. Verified: - gofmt clean - go vet ./... OK - go build ./... OK - go test ./... PASS (all ~120 pkgs) - go test -race PASS (internal/api/...) - golangci-lint clean - archmetrics no architectural regression; baseline refreshed * chore(blame): ignore R2b geofence carve 927f2b60 * refactor(R2c): carve internal/api/vehicle (core VehicleHandler) Moves the core /api/v1/vehicles handler — List, Get, Delete, Positions, CurrentState, Wake, SyncFromTesla — out of the flat parent internal/api/ into a dedicated subpackage. Sibling resources (VehicleAccessHandler, VehicleConfigHandler, VehicleInfoHandler, VehiclePhotoHandler, VehicleSettingsHandler, VehicleStatesHandler) stay in parent until R2c.1..R2c.6 micro-carves; each has its own constructor and independent route mount block so they can be carved one at a time. Carved files (2, blame preserved on both): - internal/api/vehicle_handler.go -> internal/api/vehicle/handler.go (R) - internal/api/vehicle_handler_test.go -> internal/api/vehicle/handler_test.go (R) New file: - internal/api/vehicle/doc.go - Layer/Why/TelemetryInterface/Scope Architecture decisions (precedent for sibling vehicle.* carves): - TelemetrySource interface (1 method: GetLiveSignalStore) decouples the subpkg from parent *TelemetryHandler. SetTelemetrySource(ts) replaces SetTelemetryHandler(th *TelemetryHandler) — closing the cycle without shipping a new domain port. - Local liveSignalValuesToRaw duplicate. The parent copy in signal_handler.go stays until its other callers are carved; the one-line duplication is preferable to closing a cycle on a 5-line pure helper. - fakeStateReader duplicated into handler_test.go (4 methods + 1 var assertion) for the same cycle-avoidance reason. apitest.FakeStateReader promotion is deferred to a R2.0g pre-carve when more siblings need it. Drained parent helpers: - writeAppError wrapper DELETED from helpers.go. R2c was the last caller, so the wrapper is now dead code; ADR-009 wrapper-deletion gate met for this specific helper. Other transitional wrappers (writeJSON, pagination, urlParamInt64, parseDateRange, nullableTime, writeError, writeErrorCode, writeTeslaTokenExpired) stay until their respective callers are also drained. Wiring: - router.go: added `apiveh "github.com/.../internal/api/vehicle"` import; replaced NewVehicleHandler(...) with apiveh.NewHandler(...); renamed SetTelemetryHandler call to SetTelemetrySource. Verified: - gofmt clean - go vet ./... OK - go build ./... OK - go test ./... PASS (all ~120 pkgs, no FAIL anywhere) - go test -race PASS (internal/api/...) - golangci-lint clean - archmetrics no architectural regression; baseline refreshed * chore(blame): ignore R2c vehicle carve 6efc0579 * refactor(R2c.1): carve internal/api/vehicleaccess (drivers + invitations) First VehicleHandler-sibling micro-carve after R2c core. Moves the /api/v1/vehicles/{vehicleID}/drivers and /api/v1/vehicles/{vehicleID}/invitations route clusters out of the flat parent internal/api/ into a dedicated subpackage. Carved files (1, blame preserved): - internal/api/vehicle_access_handler.go -> internal/api/vehicleaccess/handler.go (R) New file: - internal/api/vehicleaccess/doc.go - Layer/Why/Scope/Independence Why a separate subpkg (not under internal/api/vehicle/access): - Constructor takes only Tesla client + *database.DB; ZERO shared types/helpers/state with the core VehicleHandler. - Independent route mount block (/drivers, /invitations) with its own rate-limit middleware chain. - Clean carve criterion met: vehicleaccess.Handler does NOT need any symbol from internal/api/vehicle to function. truncateBody duplicate: - 6-line log-trimming helper duplicated locally from the parent internal/api/tesla_energy_history_handler.go. The parent copy stays until that handler is also carved. Wiring: - router.go: added `apivehaccess "github.com/.../internal/api/vehicleaccess"` import; replaced NewVehicleAccessHandler(teslaClient, db) with apivehaccess.NewHandler(teslaClient, db). Method calls at the route block (ListDrivers/RefreshDrivers/RemoveDriver + ListInvitations/RefreshInvitations/CreateInvitation/ RevokeInvitation) unchanged. Verified: - gofmt clean - go vet ./... OK - go build ./... OK - go test ./... PASS (all packages green, no FAIL anywhere) - go test -race PASS (internal/api/) - golangci-lint clean - archmetrics no architectural regression; baseline refreshed * chore(blame): ignore R2c.1 vehicleaccess carve 2a0ae4cc * refactor(R2c.2): carve internal/api/vehicleinfo (Tesla account metadata) Second VehicleHandler-sibling micro-carve. Moves the per-vehicle Tesla account metadata cluster — mobile-enabled status, option codes, vehicle specs, subscription eligibility, upgrade eligibility, warranty details — out of the flat parent into a dedicated subpackage. All routes stay under /api/v1/vehicles/{vehicleID}/ unchanged. Carved files (1, blame preserved): - internal/api/vehicle_info_handler.go -> internal/api/vehicleinfo/handler.go (R) New file: - internal/api/vehicleinfo/doc.go - Layer/Why/Scope/Independence Independence: - Constructor takes only Tesla client + *database.DB. ZERO shared state/types/helpers with the core VehicleHandler or any sibling. - Single router.go constructor swap; no cross-handler coupling. - 12 method receivers carved as one unit (6 read + 6 refresh pairs). Wiring: - router.go: added `apivehinfo "github.com/.../internal/api/vehicleinfo"` import; replaced NewVehicleInfoHandler(teslaClient, db) with apivehinfo.NewHandler(teslaClient, db). Route mount block unchanged (12 method handler references compatible). Verified: - gofmt clean - go vet ./... OK - go build ./... OK - go test ./... PASS (all packages green, no FAIL anywhere) - go test -race PASS (internal/api/...) - golangci-lint clean - archmetrics no architectural regression; baseline refreshed * chore(blame): ignore R2c.2 vehicleinfo carve ab372b99 * refactor(R2c.3): carve internal/api/vehicleconfig (config history + latest) Third VehicleHandler-sibling micro-carve. Moves the /api/v1/vehicle-config List + Latest handlers out of the flat parent into a dedicated subpackage. Both endpoints are backed by signal.StateReader / signal.LiveStateReader (ADR-002 / phase-39 change-feed forward-folding); the compound JSON VehicleConfig payload is flattened to top-level keys (car_type, trim_badging, exterior_color, wheel_type, ...) so the legacy wire shape is preserved. Carved files (2, blame preserved on both): - internal/api/vehicle_config_handler.go -> internal/api/vehicleconfig/handler.go (R) - internal/api/vehicle_config_handler_test.go -> internal/api/vehicleconfig/handler_test.go (R) New file: - internal/api/vehicleconfig/doc.go - Layer/Why/Scope/Independence Helpers duplicated locally: - timelineRowsToFlat (10 lines) duplicated from internal/api/drive_handler_detail.go. Parent copy stays until that handler is also carved. - fakeStateReader + newTestLiveStateReader duplicated into handler_test.go with the FULL field surface (gotTimelineOpts + gotTimelineFields) required by the chart-mode CollapseBy + signal-projection assertions. Independence: - Constructor takes only (signal.StateReader, signal.LiveStateReader) — both external. Zero coupling to sibling vehicle.* clusters. Wiring: - router.go: added `apivehconfig "github.com/.../internal/api/vehicleconfig"` import; replaced NewVehicleConfigHandler(stateReader, liveStateReader) with apivehconfig.NewHandler(...). List + Latest route mounts unchanged. Verified: - gofmt clean - go vet ./... OK - go build ./... OK - go test ./... PASS (all packages green, no FAIL anywhere) - go test -race PASS (internal/api/..., including vehicleconfig) - golangci-lint clean - archmetrics no architectural regression; baseline refreshed * chore(blame): ignore R2c.3 vehicleconfig carve c6d7985e * refactor(R2c.4): carve internal/api/vehiclestates (FSM transition views) Phase R2c.4 — fourth VehicleHandler-sibling micro-carve, lifting the vehicle-states timeline+summary HTTP handler out of the flat internal/api parent into its own subpackage. Surface ------- internal/api/vehiclestates/ doc.go — package contract: layer/why/scope/independence handler.go — Handler{Timeline, Summary} + vehicleStatesRepository narrow interface + vehicleStatesClock + window/params helpers (renamed from VehicleStatesHandler / NewVehicleStatesHandler). handler_test.go — fakeVehicleStatesRepo + 31 sub-tests (Decision #8 coverage matrix from phase-43a/0003 preserved). Wrapper migrations ------------------ - writeError(\u2026) -> httpx.WriteError(\u2026) - writeJSON(\u2026) -> httpx.WriteJSON(\u2026) - httpStatusCode(\u2026) -> httpx.HTTPStatusCode(\u2026) (used by the custom {error,code,max} envelope on days-clamp 400 so the field surface stays exactly the same.) Independence ------------ Constructor stays NewHandler(*vehicledb.VehicleStatesRepo) — narrow repo interface is satisfied by both the production *vehicledb.VehicleStatesRepo and the in-test fakeVehicleStatesRepo (compile-time conformance check preserved at the bottom of handler_test.go). vehicleStatesClock pinning for stable window boundaries is retained verbatim. Wiring ------ internal/api/router.go + import apivehstates "\u2026/internal/api/vehiclestates" - vehicleStatesHandler := NewVehicleStatesHandler(\u2026) + vehicleStatesHandler := apivehstates.NewHandler(\u2026) Routes (/api/v1/vehicle-states/timeline, /api/v1/vehicle-states/summary) unchanged. Gates ----- - gofmt -w, go vet ./... - go test -count=1 ./... PASS - go test -count=1 -race ./internal/api/... PASS - golangci-lint run \u2026/vehiclestates/... .../api/ clean - tools/archmetrics -compare OK: no architectural regression Refs: phase-R2c.4 carve, prior siblings R2c (6efc0579), R2c.1 (2a0ae4cc), R2c.2 (ab372b99), R2c.3 (c6d7985e). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2c.4 vehiclestates carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2c.5): carve internal/api/vehiclesettings (per-vehicle settings) Phase R2c.5 — fifth VehicleHandler-sibling micro-carve, lifting the per-vehicle settings handler (List / Put / Delete) out of the flat internal/api parent into its own subpackage. Surface ------- internal/api/vehiclesettings/ doc.go — package contract: layer/why/scope/independence handler.go — Handler{List, Put, Delete} plus the three injectable seams (VehicleSettingsOverrideStore, VehicleSettingsResolverInterface, VehicleExistenceChecker) and their production adapter NewVehicleExistenceChecker. Constants VehicleSettingsCode{InvalidKey, InvalidValue, NotFound, BadBody} and the 4 KiB MaxVehicleSettingsBodyBytes guard are preserved verbatim so the SPA's typed-fetch layer keeps matching on the same code strings. handler_test.go — fakeVehicleSettingsStore / Resolver / ExistenceChecker stubs + full coverage matrix. Wrapper migrations ------------------ - urlParamInt64(\u2026) -> apiparams.URLParamInt64(\u2026) - writeError(\u2026) -> httpx.WriteError(\u2026) - writeErrorCode(\u2026) -> httpx.WriteErrorCode(\u2026) - writeJSON(\u2026) -> httpx.WriteJSON(\u2026) Cross-handler dependency ------------------------ vehicle_photo_handler.go (still parent-package; R2c.6 target) uses both the VehicleExistenceChecker interface and the VehicleSettingsCodeNotFound constant for its 404 envelope. Updated those two call sites to fully-qualify against apivehsettings. fakeVehicleExistenceChecker is duplicated as a small package-private stub inside vehicle_photo_handler_test.go (Go forbids importing _test packages, so a transitional copy is the only option) — that copy vanishes at R2c.6. Wiring ------ internal/api/router.go + import apivehsettings "\u2026/internal/api/vehiclesettings" - vehicleSettingsHandler := NewVehicleSettingsHandler(\u2026) + vehicleSettingsHandler := apivehsettings.NewHandler(\u2026) - NewVehicleExistenceChecker(\u2026) (x2) + apivehsettings.NewVehicleExistenceChecker(\u2026) Routes (/api/v1/vehicles/{vehicleID}/settings[/{key}]) unchanged. Gates ----- - gofmt -w, go vet ./... - go test -count=1 ./... PASS - go test -count=1 -race ./internal/api/... PASS - golangci-lint run \u2026/vehiclesettings/... .../api/ clean - tools/archmetrics -compare OK: no architectural regression Refs: phase-R2c.5 carve. Prior siblings: R2c (6efc0579), R2c.1 (2a0ae4cc), R2c.2 (ab372b99), R2c.3 (c6d7985e), R2c.4 (7670a18e). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2c.5 vehiclesettings carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2c.6): carve internal/api/vehiclephoto (photo upload + serve) Phase R2c.6 — final VehicleHandler-sibling micro-carve, lifting the four photo endpoints (POST/GET meta, GET file, DELETE) plus the on-disk encode/atomic-write pipeline out of the flat internal/api parent into its own subpackage. With this carve the original VehicleHandler cluster (7 independent handler types in 12 files spanning ~167 KB) is fully decomposed: R2c internal/api/vehicle (core) R2c.1 internal/api/vehicleaccess (drivers + invitations) R2c.2 internal/api/vehicleinfo (Tesla account metadata) R2c.3 internal/api/vehicleconfig (config history + latest) R2c.4 internal/api/vehiclestates (FSM transition views) R2c.5 internal/api/vehiclesettings (per-vehicle settings KV) R2c.6 internal/api/vehiclephoto (photo pipeline) \u2190 this commit Surface ------- internal/api/vehiclephoto/ doc.go — package contract; documents the one-way vehiclephoto -> vehiclesettings dep for the shared VehicleExistenceChecker seam + VEHICLE_NOT_FOUND envelope code. handler.go — Handler{GetMeta, GetFile, Upload, Delete} + per-vehicle upload mutex map; on-disk pipeline (writeAtomicJPEG, resolveSafePath traversal guard, cleanupStaged, removeEmptyParent); local isMaxBytesError duplicate (still consumed by parent notification/webhook handlers); new exported IsUploadPath(method,path) helper used by the router's global body-limit bypass. Constants MaxUploadBytes, PhotoSize*, PhotoMaxDimByName, PhotoSizesOrdered, AllowedPhotoMimeTypes, VehiclePhotoUploadFormField, and PhotoCode* preserved verbatim for SPA parity. handler_test.go — fakeVehiclePhotoStore + minimal local fakeVehicleExistenceChecker (Go forbids importing _test packages, so the duplicate that landed transitionally in R2c.5 stays here permanently); full coverage matrix retained, real on-disk root under t.TempDir() so the encode + write pipeline is exercised end-to-end. Wrapper migrations ------------------ - urlParamInt64(\u2026) -> apiparams.URLParamInt64(\u2026) - writeError(\u2026) -> httpx.WriteError(\u2026) - writeErrorCode(\u2026) -> httpx.WriteErrorCode(\u2026) - writeJSON(\u2026) -> httpx.WriteJSON(\u2026) Cross-package dependency ------------------------ vehiclephoto -> vehiclesettings (one-way) for VehicleExistenceChecker (the seam) and VehicleSettingsCodeNotFound (the 404 envelope code string). The SPA's typed-fetch layer keys on the same code string regardless of which handler emits it. Body-limit bypass move ---------------------- isVehiclePhotoUploadPath(method, path) — the helper consumed by the global body-limit middleware to widen the 1 MB cap to 12 MB for POST /api/v1/vehicles/{id}/photo — was a router.go-local function. Promoted into the subpackage as exported IsUploadPath so the middleware can reach it without re-implementing the route shape, and the table-driven test (TestIsUploadPath...) moves along with the function. router.go now calls apivehphoto.IsUploadPath; the local copy in router.go is deleted. Wiring ------ internal/api/router.go + import apivehphoto "\u2026/internal/api/vehiclephoto" - vehiclePhotoHandler := NewVehiclePhotoHandler(\u2026) + vehiclePhotoHandler := apivehphoto.NewHandler(\u2026) - if isVehiclePhotoUploadPath(req.Method, req.URL.Path) + if apivehphoto.IsUploadPath(req.Method, req.URL.Path) - func isVehiclePhotoUploadPath(\u2026) bool { \u2026 } (deleted) Routes (/api/v1/vehicles/{vehicleID}/photo[/{size}]) unchanged. Gates ----- - gofmt -w, go vet ./... - go test -count=1 ./... PASS - go test -count=1 -race ./internal/api/... PASS - golangci-lint run \u2026/vehiclephoto/... .../api/ clean - tools/archmetrics -compare OK: no architectural regression Refs: phase-R2c.6 carve, completes the VehicleHandler decomposition begun at R2c (6efc0579). Sibling SHAs: R2c.1 2a0ae4cc, R2c.2 ab372b99, R2c.3 c6d7985e, R2c.4 7670a18e, R2c.5 737d3770. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2c.6 vehiclephoto carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.1): carve internal/api/search subpackage First non-vehicle resource carve. Moves SearchHandler + PGSearcher into internal/api/search/ as Handler / PGSearcher (exported because two AI hydrators consume it directly). Introduces internal/api/search/searchtest subpackage exporting FakeSearcher so cross-package tests (ai_search, ai_drive_search) can share the fake — Go forbids importing _test packages. Files carved: - internal/api/search_handler.go -> internal/api/search/handler.go - internal/api/search_handler_test.go -> internal/api/search/handler_test.go New: - internal/api/search/doc.go (Layer: handler) - internal/api/search/searchtest/fake.go (Layer: platform) - internal/api/search/searchtest/doc.go Patched parent files: - ai_search_hydrator.go, ai_drive_search_hydrator.go: import apisearch; Searcher/SearchHit -> apisearch.X - ai_search_handler_test.go, ai_drive_search_handler_test.go: import apisearch + searchtest; newFakeSearcher -> searchtest.NewFakeSearcher; SearchType*/SearchHit/NewSearchHandlerWithSearcher -> apisearch.X; .hits[ -> .Hits[, .errs[ -> .Errs[ - router.go: import apisearch; NewSearchHandler -> apisearch.NewHandler; 2x newPGSearcher -> apisearch.NewPGSearcher Renames inside subpkg: - SearchHandler -> Handler - NewSearchHandler -> NewHandler - NewSearchHandlerWithSearcher -> NewHandlerWithSearcher - pgSearcher -> PGSearcher (exported for hydrator wiring) - newPGSearcher -> NewPGSearcher - SearchType* constants unchanged (already exported) Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Gates: go vet clean, go test ./... PASS, -race ./internal/api/... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.1 search carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.2): carve internal/api/lifetime subpackage Moves the /analytics/lifetime handler + ComputeLifetimeStats helper into internal/api/lifetime/ as Handler / NewHandler. The package-level ComputeLifetimeStats remains exported because the AI strategy ai_lifetime_stats_qa consumes it directly to ground its Q&A in the same deterministic envelope the chart renders. Files carved: - internal/api/lifetime_handler.go -> internal/api/lifetime/handler.go - internal/api/lifetime_handler_test.go -> internal/api/lifetime/handler_test.go New: - internal/api/lifetime/doc.go (Layer: handler) Renames inside subpkg: - LifetimeHandler -> Handler - NewLifetimeHandler -> NewHandler - achievementEventBroadcaster (internal interface) -> EventBroadcaster (exported port — parent *api.EventHub auto-satisfies it via BroadcastWithContext, so the subpackage takes ZERO dependency on the SSE hub concrete type) - Other exported symbols unchanged: ComputeLifetimeStats, LifetimeStatsResult, Achievement, PersonalRecord Patched parent files: - router.go: import apilifetime; NewLifetimeHandler -> apilifetime.NewHandler - ai_lifetime_stats_qa_handler.go: import apilifetime (aliased to avoid collision with existing internal/ai/tools/lifetime import); ComputeLifetimeStats -> apilifetime.ComputeLifetimeStats Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Local helper: safeFloat duplicated in subpkg (parent api.safeFloat lives in converters.go and is reused by many other handlers — duplicating keeps the subpackage free of any dependency on the parent's converters). Gates: go vet clean, go test ./... PASS, -race ./internal/api/... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.2 lifetime carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.3): carve internal/api/notification subpackage Moves the entire notification cluster (3 handlers, 2 test files) into internal/api/notification/. Largest carve in the R2 series so far at ~72KB across 5 files. Files carved: - notification_handler.go -> notification/handler.go (Handler) - notification_channel_handler.go-> notification/channel.go (ChannelHandler) - notification_schedule_handler.go-> notification/schedule.go (ScheduleHandler) - notification_handler_test.go -> notification/handler_test.go - notification_channel_handler_test.go -> notification/channel_test.go New: - internal/api/notification/doc.go (Layer: handler) - internal/api/notification/helpers.go (local copies of isMaxBytesError + boolPtr — parent originals stay alive because webhook_receiver and telemetry_sessions_signal_helpers still use them) Renames inside subpkg: - NotificationHandler -> Handler - NewNotificationHandler -> NewHandler - NotificationChannelHandler -> ChannelHandler - NewNotificationChannelHandler -> NewChannelHandler - NotificationScheduleHandler -> ScheduleHandler - NewNotificationScheduleHandler -> NewScheduleHandler Outbound API call sink: The notification adapters (Discord/Slack/Telegram/Webhook/Ntfy/Pushover) read currentOutboundSink on every call so the most-recent SetOutboundSink wins. To avoid the subpackage taking a dependency on system_handler.go, the subpackage exposes a package var SinkProvider func() httputil.APICallSink which the composition root sets at boot: apinotif.SinkProvider = currentOutboundSink The system_handler comment is updated to point at the new wiring. Patched parent files: - router.go: import apinotif; 3x NewX swaps; wire SinkProvider - system_handler.go: doc comment update for SetOutboundSink Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON, urlParamInt64 -> apiparams.URLParamInt64, pagination -> apiparams.Pagination. Internal adapter helpers move with the handler (no external consumers): sendDiscord, sendSlack, sendTelegram, sendWebhook, sendNtfy, sendPushover, postJSON, notifyOutboundClient, normalizeChannelResponse. Gates: go vet clean, go test ./... PASS, -race ./internal/api/... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.3 notification carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.4): carve internal/api/signalinspect subpackage Moves the per-vehicle signal-inspector handler + the proto-derived signals catalog into internal/api/signalinspect/. The subpackage name is signalinspect (NOT signal) to avoid collision with the internal/signal package both files import. Files carved: - signal_handler.go -> signalinspect/handler.go (Handler) - signal_handler_test.go -> signalinspect/handler_test.go - signals.go -> signalinspect/catalog.go (AvailableSignals + SubscribedSignals) New: - internal/api/signalinspect/doc.go (Layer: handler) Renames inside subpkg: - SignalHandler -> Handler - NewSignalHandler -> NewHandler - liveSignalValuesToRaw -> LiveSignalValuesToRaw (promoted to exported because alert_handler_rules.go calls it from the parent package) Already-exported (unchanged): AvailableSignal, AvailableSignals, SubscribedSignals, WithDB, WithSignalHistory, WithRedisCache, WithLiveSignalStore, LiveState, Snapshot, Diff, AvailableSignals method, Stats, History. Patched parent files: - router.go: import apisignal; 2x NewSignalHandler -> apisignal.NewHandler - ai_signal_explorer_nl_filter_handler.go: import apisignal; AvailableSignals() -> apisignal.AvailableSignals() - alert_handler_rules.go: import apisignal; liveSignalValuesToRaw -> apisignal.LiveSignalValuesToRaw - health.go: import apisignal; SubscribedSignals -> apisignal.SubscribedSignals Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON (13 + 15 call sites). The signals_catalog_handler.go (separate cluster, different responsibility — catalog aggregates + observations from signal_log, not per-vehicle live inspection) is NOT touched by this carve. Gates: go vet clean, go test ./... PASS, -race ./internal/api/... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.4 signalinspect carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.5): carve internal/api/signalscatalog subpackage Moves the /signals/catalog + /signals/observations global endpoints into internal/api/signalscatalog/. Distinct from internal/api/signalinspect (R2d.4) which serves the per-vehicle live-inspector endpoints; catalog + observations are global (no vehicle scope) and are an ADR-009 exception restoration introduced by Phase-43a / Prompt 0007. Files carved: - signals_catalog_handler.go -> signalscatalog/handler.go (Handler) - signals_catalog_handler_test.go -> signalscatalog/handler_test.go New: - internal/api/signalscatalog/doc.go (Layer: handler) Renames inside subpkg: - SignalsCatalogHandler -> Handler - NewSignalsCatalogHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError (11x), writeJSON -> httpx.WriteJSON (3x), httpStatusCode -> httpx.HTTPStatusCode. Patched parent file: - router.go: import apisigcat; NewSignalsCatalogHandler -> apisigcat.NewHandler Gates: go vet clean, go test ./... PASS, -race ./internal/api/... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.5 signalscatalog carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.6): carve internal/api/openapi subpackage Moves the GET /api/v1/system/openapi YAML spec endpoint into internal/api/openapi/. Tiny standalone carve — single handler with no cross-package consumers other than the composition root. Files carved: - openapi_handler.go -> openapi/handler.go (Handler) New: - internal/api/openapi/doc.go (Layer: handler) Renames inside subpkg: - OpenAPIHandler -> Handler (constructor function returning http.HandlerFunc) - SetOpenAPISpec -> SetOpenAPISpec (unchanged; package qualifier disambiguates) Wrapper swaps: writeError -> httpx.WriteError. Patched parent files: - router.go: import apiopenapi; OpenAPIHandler() -> apiopenapi.Handler() - internal/app/new.go: import apiopenapi; api.SetOpenAPISpec -> apiopenapi.SetOpenAPISpec at composition-root load site. Gates: go vet clean, go test ./... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.6 openapi carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.7): carve internal/api/synthetic subpackage Moves the GET /api/v1/admin/observability/synthetic endpoint (ADR-009 exception) into internal/api/synthetic/. Tiny single-handler carve — exposes a snapshot of every registered synthetic-monitoring probe. Files carved: - synthetic_handler.go -> synthetic/handler.go (Handler) New: - internal/api/synthetic/doc.go (Layer: handler) Renames inside subpkg: - SyntheticHandler -> Handler - NewSyntheticHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Import-name collision handled: the subpackage is named `synthetic` but must consume internal/synthetic.Runner — aliased as `synthrun` inside handler.go to disambiguate from the surrounding package name. Patched parent files: - router.go: import apisynthetic; NewSyntheticHandler(opt.SyntheticRunner) -> apisynthetic.NewHandler(opt.SyntheticRunner) Gates: go vet clean, go test ./... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.7 synthetic carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.8): carve internal/api/visitedlocation subpackage Moves GET /api/v1/locations into internal/api/visitedlocation/. Tiny single-handler carve — read-only list of locations the fleet has visited, with optional vehicle_id query-string scoping. Files carved: - visited_location_handler.go -> visitedlocation/handler.go (Handler) New: - internal/api/visitedlocation/doc.go (Layer: handler) Renames inside subpkg: - VisitedLocationHandler -> Handler - NewVisitedLocationHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON, pagination -> apiparams.Pagination. Patched parent files: - router.go: import apivisloc; NewVisitedLocationHandler(db) -> apivisloc.NewHandler(db). Gates: go vet clean, go test ./... PASS, golangci-lint clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.8 visitedlocation carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.9): carve internal/api/apicalllog subpackage Moves the GET /api/v1/api-logs and /api-logs/stats read endpoints into internal/api/apicalllog/. Tiny standalone carve — the writes side (APICallLogMiddleware + GetAPICallLogger) intentionally stays in the parent internal/api package because it is part of the chi middleware chain wired by router.go; only the read handler is carved. Files carved: - api_call_log_handler.go -> apicalllog/handler.go (Handler) New: - internal/api/apicalllog/doc.go (Layer: handler) Renames inside subpkg: - APICallLogHandler -> Handler - NewAPICallLogHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON, pagination -> apiparams.Pagination. Patched parent files: - router.go: import apicalllog; NewAPICallLogHandler(db) -> apicalllog.NewHandler(db). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.9 apicalllog carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.10): carve internal/api/slo subpackage Moves the GET /api/v1/admin/observability/slo endpoint (ADR-009 exception) into internal/api/slo/. Tiny single-handler carve — returns one row per SLO declared in slo/catalog.yaml with live SLI ratio, error budget remaining, and per-tier burn-rate evaluation. Files carved: - slo_handler.go -> slo/handler.go (Handler) New: - internal/api/slo/doc.go (Layer: handler) Renames inside subpkg: - SLOHandler -> Handler - NewSLOHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Import-name collision handled: the subpackage is named `slo` but must consume internal/slo.{Catalog,Tracker} — aliased as `slopkg` inside handler.go to disambiguate from the surrounding package name. Patched parent files: - router.go: import apislo; NewSLOHandler(opt.SLOCatalog, opt.SLOTracker) -> apislo.NewHandler(opt.SLOCatalog, opt.SLOTracker). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.10 slo carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.11): carve internal/api/geocode subpackage Moves GET /api/v1/geocode/search and /geocode/reverse into internal/api/geocode/. Tiny standalone carve — two read-only endpoints both rate-limited at the router (30/min) over Nominatim (forward) and the configured commercial reverse provider (Google or Azure Maps). Files carved: - geocode_handler.go -> geocode/handler.go (Handler) New: - internal/api/geocode/doc.go (Layer: handler) Renames inside subpkg: - GeocodeHandler -> Handler - NewGeocodeHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Patched parent files: - router.go: import apigeocode; NewGeocodeHandler(...) -> apigeocode.NewHandler(...). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.11 geocode carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.12): carve internal/api/dataquality subpackage Moves the GET /admin/observability/data-quality + /lineage endpoints (ADR-009 exception) into internal/api/dataquality/. Tiny standalone carve — per-field freshness/max-gap/duplicate-ratio scoring over signal_log plus a static pipeline DAG. Files carved: - dataquality_handler.go -> dataquality/handler.go (Handler) New: - internal/api/dataquality/doc.go (Layer: handler) Renames inside subpkg: - DataQualityHandler -> Handler - NewDataQualityHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Import-name collision handled: subpkg is named `dataquality` but must consume internal/dataquality.{Scorer,ErrNotConfigured,BuildLineage} — aliased as `dqpkg` inside handler.go. Patched parent files: - router.go: import apidq; NewDataQualityHandler(opt.DataQualityScorer) -> apidq.NewHandler(opt.DataQualityScorer). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.12 dataquality carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.13): carve internal/api/softwareupdate subpackage Moves GET /api/v1/software-updates into internal/api/softwareupdate/. Tiny single-handler carve — durable history of Tesla over-the-air firmware updates with optional vehicle_id scoping plus standard start/end date range and limit pagination. Files carved: - software_update_handler.go -> softwareupdate/handler.go (Handler) New: - internal/api/softwareupdate/doc.go (Layer: handler) Renames inside subpkg: - SoftwareUpdateHandler -> Handler - NewSoftwareUpdateHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON, pagination -> apiparams.Pagination, parseDateRange -> apiparams.ParseDateRange. Patched parent files: - router.go: import apisoftupd; NewSoftwareUpdateHandler(db) -> apisoftupd.NewHandler(db). - ai_software_update_changelog_summarizer_handler.go: docstring `SoftwareUpdateHandler` -> `Handler` (comment only). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.13 softwareupdate carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.14): carve internal/api/exportcolumns subpackage Moves GET /api/v1/exports/columns into internal/api/exportcolumns/. Tiny standalone carve — returns publishable column metadata for each export job type so the frontend column picker can render checkboxes without hard-coding the catalog (Phase-46 / Prompt 62). Files carved: - exports_columns_handler.go -> exportcolumns/handler.go (Handler) - exports_columns_handler_test.go -> exportcolumns/handler_test.go New: - internal/api/exportcolumns/doc.go (Layer: handler) Renames inside subpkg: - ExportColumnsHandler -> Handler - NewExportColumnsHandler -> NewHandler - TestExportColumnsHandler_ListColumns -> TestHandler_ListColumns Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON. Patched parent files: - router.go: import apiexpcol; NewExportColumnsHandler() -> apiexpcol.NewHandler(). Gates: go vet clean, go test ./... PASS (internal/api/exportcolumns 0.191s), golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.14 exportcolumns carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.15): carve internal/api/weeklydigest subpackage Moves GET /api/v1/vehicles/{vehicleID}/weekly-digest into internal/api/weeklydigest/. Tiny single-handler carve — returns aggregated stats (drives, distance, energy, cost, efficiency) for the current vs previous week. Reads Phase-42 SI canonical drives table (distance_m, energy_used_wh) and converts to km/kWh on the wire to preserve the legacy frontend contract. Files carved: - weekly_digest_handler.go -> weeklydigest/handler.go (Handler) New: - internal/api/weeklydigest/doc.go (Layer: handler) Renames inside subpkg: - WeeklyDigestHandler -> Handler - NewWeeklyDigestHandler -> NewHandler Wrapper swaps: writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON, urlParamInt64 -> apiparams.URLParamInt64. Patched parent files: - router.go: import apiweekly; NewWeeklyDigestHandler(db) -> apiweekly.NewHandler(db). Gates: go vet clean, go test ./... PASS, golangci-lint clean, gofmt clean, archmetrics no regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.15 weeklydigest carve in git blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.16): carve internal/api/tco subpackage Splits the TCO (Total Cost of Ownership) handler off the flat internal/api/ parent package into its own subpackage: internal/api/tco/ doc.go (new — Layer: handler) handler.go (was tco_handler.go) summary.go (was tco_summary.go) summary_test.go (was tco_summary_test.go) * TCOHandler renamed to Handler; NewTCOHandler renamed to NewHandler per the established carve naming convention. * httpx wrapper swaps applied (writeError -> httpx.WriteError, writeJSON -> httpx.WriteJSON). * Shared helpers ComputeTCOSummary, TCOSummary, TCOMonthlyEntry stay exported so the AI consumer (ai_tco_narration_handler.go, which remains in the parent package) can still reach them via the new apitco import alias. * godoc cross-references updated from *TCOHandler.GetTCO to *Handler.GetTCO inside the new subpackage. * Router (internal/api/router.go) and ai_tco_narration_handler.go updated to import the new subpackage with alias 'apitco' and call apitco.NewHandler / apitco.ComputeTCOSummary. No behavior change. archmetrics baseline refreshed: no architectural regression. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: ignore R2d.16 tco carve in git-blame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.19): carve internal/api/onboarding subpackage Splits the onboarding handler off the flat internal/api/ parent package into its own subpackage: internal/api/onboarding/ doc.go (new - Layer: handler) handler.go (was onboarding_handler.go) handler_test.go (was onboarding_handler_test.go) * OnboardingHandler renamed to Handler; NewOnboardingHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apionboard' and call apionboard.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.18): carve internal/api/teslauserprofile subpackage Splits the Tesla user profile handler off the flat internal/api/ parent package into its own subpackage: internal/api/teslauserprofile/ doc.go (new - Layer: handler) handler.go (was tesla_user_profile_handler.go) * TeslaUserProfileHandler renamed to Handler; NewTeslaUserProfileHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apitup' and call apitup.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(R2d.18-19): refresh archmetrics baseline + ignore fleet carves in git-blame First successful parallel-fleet batch: R2d.18 (teslauserprofile) and R2d.19 (onboarding) carved in separate git worktrees by background agents, then cherry-picked into chore/repo-reorganization. R2d.17 (settingsexport) deferred — needs joint carve with settings_import due to shared fakeSettingsRepo/fakeAlertRepo/fakeGeofenceRepo/ fakeQuietHoursRepo + newTestSettingsSerializer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.20): carve internal/api/trip subpackage Splits the trip handler off the flat internal/api/ parent package into its own subpackage: internal/api/trip/ doc.go (new - Layer: handler) handler.go (was trip_handler.go) * TripHandler renamed to Handler; NewTripHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apitrip' and call apitrip.NewHandler. * Distinct from internal/handler/v1.TripHandler (separate package, untouched). No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.21): carve internal/api/webhookreceiver subpackage Splits the webhook receiver handler off the flat internal/api/ parent package into its own subpackage: internal/api/webhookreceiver/ doc.go (new - Layer: handler) handler.go (was webhook_receiver_handler.go) handler_test.go (was webhook_receiver_handler_test.go) * WebhookReceiverHandler renamed to Handler; NewWebhookReceiverHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apiwhrx' and call apiwhrx.NewHandler. * Distinct from the dead webhook_handler.go (slated for deletion in a later Phase R cleanup). No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(R2d.20-21): refresh archmetrics baseline + ignore fleet batch 2 in git-blame Second successful parallel-fleet batch: R2d.20 (trip) and R2d.21 (webhookreceiver) carved in separate git worktrees by background agents, then cherry-picked into chore/repo-reorganization. Webhookreceiver agent also patched a router_middleware.go parent consumer that the pre-fleet survey had missed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.22): carve internal/api/teslauserorder subpackage Splits the Tesla user order handler off the flat internal/api/ parent package into its own subpackage: internal/api/teslauserorder/ doc.go (new - Layer: handler) handler.go (was tesla_user_order_handler.go) * TeslaUserOrderHandler renamed to Handler; NewTeslaUserOrderHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apituo' and call apituo.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.23): carve internal/api/teslauserconfig subpackage Splits the Tesla user config handler off the flat internal/api/ parent package into its own subpackage: internal/api/teslauserconfig/ doc.go (new - Layer: handler) handler.go (was tesla_user_config_handler.go) * TeslaUserConfigHandler renamed to Handler; NewTeslaUserConfigHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apituc' and call apituc.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.24): carve internal/api/gasprice subpackage Splits the gas price handler off the flat internal/api/ parent package into its own subpackage: internal/api/gasprice/ doc.go (new - Layer: handler) handler.go (was gas_price_handler.go) * GasPriceHandler renamed to Handler; NewGasPriceHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apigas' and call apigas.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.25): carve internal/api/apikey subpackage Splits the API key handler off the flat internal/api/ parent package into its own subpackage: internal/api/apikey/ doc.go (new - Layer: handler) handler.go (was apikey_handler.go) * APIKeyHandler renamed to Handler; NewAPIKeyHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apikeyh' and call apikeyh.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.26): carve internal/api/feedback subpackage Splits the feedback handler off the flat internal/api/ parent package into its own subpackage: internal/api/feedback/ doc.go (new - Layer: handler) handler.go (was feedback_handler.go) handler_test.go (was feedback_handler_test.go) * FeedbackHandler renamed to Handler; NewFeedbackHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apifb' and call apifb.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(R2d.22-26): refresh archmetrics baseline + ignore fleet batch 3 in git-blame Third parallel-fleet batch: 5 carves in parallel via background agents in separate git worktrees. Two minor merge conflicts on router.go (overlapping import insertion + ctor call regions) resolved trivially by keeping both. Carves: R2d.22 teslauserorder R2d.23 teslauserconfig R2d.24 gasprice R2d.25 apikey (also patched apikey_middleware.go) R2d.26 feedback (also patched helpers.go - kept firstNonEmpty for other consumers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.31): carve internal/api/drivediagnostic subpackage Splits the drive diagnostic handler off the flat internal/api/ parent package into its own subpackage: internal/api/drivediagnostic/ doc.go (new - Layer: handler) handler.go (was drive_diagnostic_handler.go) handler_test.go (was drive_diagnostic_handler_test.go) * DriveDiagnosticHandler renamed to Handler; NewDriveDiagnosticHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apidrived' and call apidrived.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.30): carve internal/api/user subpackage Splits the user handler off the flat internal/api/ parent package into its own subpackage: internal/api/user/ doc.go (new - Layer: handler) handler.go (was user_handler.go) * UserHandler renamed to Handler; NewUserHandler renamed to NewHandler per the established carve naming convention. * httpx wrapper swaps applied. * Router had no flat NewUserHandler consumer at HEAD; internal/handler/v1.UserHandler remains untouched. * Distinct from internal/handler/v1.UserHandler (separate package, untouched). No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.28): carve internal/api/teslaenergylivestatus subpackage Splits the Tesla energy live status handler off the flat internal/api/ parent package into its own subpackage: internal/api/teslaenergylivestatus/ doc.go (new - Layer: handler) handler.go (was tesla_energy_live_status_handler.go) * TeslaEnergyLiveStatusHandler renamed to Handler; NewTeslaEnergyLiveStatusHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apitels' and call apitels.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.27): carve internal/api/auth subpackage Splits the auth handler off the flat internal/api/ parent package into its own subpackage: internal/api/auth/ doc.go (new - Layer: handler) handler.go (was auth_handler.go) * AuthHandler renamed to Handler; NewAuthHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apiauth' and call apiauth.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.29): carve internal/api/periodstats subpackage Splits the period stats handler off the flat internal/api/ parent package into its own subpackage: internal/api/periodstats/ doc.go (new - Layer: handler) handler.go (was period_stats_handler.go) * PeriodStatsHandler renamed to Handler; NewPeriodStatsHandler renamed to NewHandler per the established carve naming convention. * httpx + apiparams wrapper swaps applied. * Router updated to import alias 'apiperiod' and call apiperiod.NewHandler. No behavior change. Pure code move + package split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(R2d.27-31): refresh archmetrics baseline + ignore fleet batch 4 in git-blame Fleet batch 4 carves (parallel worktrees, cherry-picked into main): - R2d.27 auth (0e758201) - R2d.28 teslaenergylivestatus (9f630e42) - R2d.29 periodstats (04373b2e, exports ComputePeriodStats) - R2d.30 user (0aade829, orphan handler — no router change) - R2d.31 drivediagnostic (c745a5a7) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.32): carve internal/api/ingestxray subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.33): carve internal/api/webvitals subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.34): carve internal/api/weberrors subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.35): carve internal/api/pinned subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.36): carve internal/api/dlq subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.37): carve internal/api/impersonate subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(R2d.32-37): refresh archmetrics baseline + ignore fleet batch 5 in git-blame Fleet batch 5 (6 parallel carves, scaled up from 5): - R2d.32 ingestxray (4a1867c0) - R2d.33 webvitals (ce883968, exports NormalizeRoute) - R2d.34 weberrors (fb365cea, integration: dedup local normalizeWebVitalsRoute → apivitals.NormalizeRoute) - R2d.35 pinned (553b88f8, patched saved_views_handler_test.go) - R2d.36 dlq (cd8536fe, patched flags_handler.go) - R2d.37 impersonate (28818b2d, patched ai_pii_redaction_shared_exports_handler.go) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.38): carve internal/api/tripsdetail subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.39): carve internal/api/authsession subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.40): carve internal/api/quiethours subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.41): carve internal/api/apiflagsh subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.42): carve internal/api/sysauthmode subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.43): carve internal/api/ratelimit subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.44): carve internal/api/savedviews subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.47): carve internal/api/diagnostic subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.45): carve internal/api/mileage subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(R2d.46): carve internal/api/anomaly subpackage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.co…
1 parent e155065 commit 04fc5f8

3,144 files changed

Lines changed: 236844 additions & 213664 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# EditorConfig — https://editorconfig.org/
2+
#
3+
# Phase A2.1 of chore/repo-reorganization. Codifies the formatting
4+
# conventions that have grown organically across the TeslaSync codebase
5+
# so editors apply them automatically, without depending on per-language
6+
# tooling (gofmt, prettier, etc.) running on save.
7+
#
8+
# This file is read by the editor BEFORE the file is opened, so it
9+
# affects what gets COMMITTED, not just what gets rendered. Per-language
10+
# tools (gofmt, eslint, etc.) still run as authoritative gates in CI;
11+
# .editorconfig just stops mixed-CRLF / mixed-indent commits from being
12+
# introduced in the first place.
13+
14+
root = true
15+
16+
[*]
17+
charset = utf-8
18+
end_of_line = lf
19+
insert_final_newline = true
20+
trim_trailing_whitespace = true
21+
indent_style = space
22+
indent_size = 2
23+
24+
# Go: gofmt uses tabs (width is editor preference; 4 is common)
25+
[*.go]
26+
indent_style = tab
27+
indent_size = 4
28+
29+
# Go modules: tab-indented per `go mod tidy` output
30+
[go.{mod,sum}]
31+
indent_style = tab
32+
indent_size = 4
33+
34+
# Makefiles: REQUIRE real tab indentation (recipe lines won't run otherwise)
35+
[{Makefile,*.mk}]
36+
indent_style = tab
37+
indent_size = 4
38+
39+
# Shell scripts: 2-space convention used across scripts/ and pre-commit hooks
40+
[*.{sh,bash}]
41+
indent_style = space
42+
indent_size = 2
43+
44+
# Python (pre-commit hooks, occasional tooling): PEP 8 = 4 spaces
45+
[*.py]
46+
indent_style = space
47+
indent_size = 4
48+
49+
# YAML: 2 spaces, no tabs (yaml-spec compliant; CI configs assume this)
50+
[*.{yml,yaml}]
51+
indent_style = space
52+
indent_size = 2
53+
54+
# JSON / JSONC: 2 spaces, common across tsconfig.json + package.json
55+
[*.{json,jsonc}]
56+
indent_style = space
57+
indent_size = 2
58+
59+
# TypeScript / JavaScript / JSX: ESLint formatter expects 2 spaces
60+
[*.{ts,tsx,js,jsx,mjs,cjs}]
61+
indent_style = space
62+
indent_size = 2
63+
64+
# CSS / SCSS / HTML: 2 spaces
65+
[*.{css,scss,html,vue}]
66+
indent_style = space
67+
indent_size = 2
68+
69+
# SQL: 2 spaces (migrations/ and db/seeds/ convention)
70+
[*.sql]
71+
indent_style = space
72+
indent_size = 2
73+
74+
# Markdown: preserve trailing whitespace (used for hard line breaks)
75+
[*.md]
76+
trim_trailing_whitespace = false
77+
78+
# Vendored / generated / third-party — opt out of normalization to avoid
79+
# spurious diffs against upstream.
80+
[{web/node_modules,web/dist,bin,coverage,test-results,tmp}/**]
81+
indent_style = unset
82+
indent_size = unset
83+
end_of_line = unset
84+
trim_trailing_whitespace = unset
85+
insert_final_newline = unset
86+
87+
[**/vendor/**]
88+
indent_style = unset
89+
indent_size = unset
90+
end_of_line = unset
91+
trim_trailing_whitespace = unset
92+
insert_final_newline = unset
93+
94+
[internal/tesla/protomodel/**.go]
95+
# Generated by `go generate ./internal/tesla/protomodel/...` from the
96+
# vendored Tesla proto. Don't fight the generator's formatting.
97+
trim_trailing_whitespace = false
98+
99+
[web/src/generated/**]
100+
# Generated by `go run ./tools/aigen` (Phase-50 / 0001 — AI-Off Contract).
101+
# Don't fight the generator.
102+
trim_trailing_whitespace = false

.git-blame-ignore-revs

Lines changed: 425 additions & 0 deletions
Large diffs are not rendered by default.

.gitattributes

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,75 @@ api/proto/tesla/vehicle_data.proto binary
88
# every `go generate` and trips the phase-42 prompt 0015 drift gate.
99
internal/tesla/protomodel/*_gen.go text eol=lf
1010
cmd/protogen-tesla/testdata/golden/*_gen.go text eol=lf
11+
12+
# Phase A2.3 (chore/repo-reorganization) — defend against Windows
13+
# CRLF artifacts in text source files. The repo is gofmt-clean on
14+
# Linux (where CI runs), but Windows checkouts with the default
15+
# PowerShell Out-File behaviour, and any Windows IDE/tool that writes
16+
# without explicitly opting into LF, would leave the working tree in
17+
# CRLF while the blob stays LF. That made `gofmt -l` falsely report
18+
# ~900 "needing formatting" because gofmt reads CRLF from disk and
19+
# writes LF. Forcing eol=lf on every text source language we ship
20+
# eliminates the false positive without touching the blobs.
21+
*.go text eol=lf
22+
*.mod text eol=lf
23+
*.sum text eol=lf
24+
*.ts text eol=lf
25+
*.tsx text eol=lf
26+
*.js text eol=lf
27+
*.jsx text eol=lf
28+
*.mjs text eol=lf
29+
*.cjs text eol=lf
30+
*.json text eol=lf
31+
*.jsonc text eol=lf
32+
*.yml text eol=lf
33+
*.yaml text eol=lf
34+
*.toml text eol=lf
35+
*.md text eol=lf
36+
*.sql text eol=lf
37+
*.sh text eol=lf
38+
*.bash text eol=lf
39+
*.py text eol=lf
40+
*.css text eol=lf
41+
*.scss text eol=lf
42+
*.html text eol=lf
43+
*.svg text eol=lf
44+
Makefile text eol=lf
45+
*.mk text eol=lf
46+
Dockerfile* text eol=lf
47+
.editorconfig text eol=lf
48+
.gitignore text eol=lf
49+
.gitattributes text eol=lf
50+
.env* text eol=lf
51+
52+
# Windows-shipped artifacts: keep native line endings.
53+
*.ps1 text eol=crlf
54+
*.cmd text eol=crlf
55+
*.bat text eol=crlf
56+
57+
# Pure binaries: never normalise.
58+
*.png binary
59+
*.jpg binary
60+
*.jpeg binary
61+
*.gif binary
62+
*.ico binary
63+
*.webp binary
64+
*.woff binary
65+
*.woff2 binary
66+
*.ttf binary
67+
*.eot binary
68+
*.pdf binary
69+
*.zip binary
70+
*.tar binary
71+
*.tar.gz binary
72+
*.gz binary
73+
*.exe binary
74+
*.dll binary
75+
*.so binary
76+
*.dylib binary
77+
# .proto files: marked binary so they stay byte-exact. The vendored
78+
# Tesla proto at api/proto/tesla/vehicle_data.proto is also explicitly
79+
# marked above (top of file) for clarity; the rule below covers the
80+
# generic case for any other proto we may vendor.
81+
*.proto binary
82+

0 commit comments

Comments
 (0)