Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
e499c59
feat: add ScmsHealthDto, ScmsHealthMapper, and unit tests for SCMS he…
dmccoystephenson Mar 27, 2026
6fb0329
feat: add ScmsHealthService with method skeleton for SCMS health stat…
dmccoystephenson Mar 27, 2026
c5863c1
feat: add ScmsHealthController and integration tests for SCMS health …
dmccoystephenson Mar 27, 2026
253471f
feat: update ScmsHealthDto, mapper, and controller to return SCMS hea…
dmccoystephenson Mar 27, 2026
82d1d14
feat: implement SCMS health status retrieval with repository, service…
dmccoystephenson Mar 27, 2026
002da56
test: add test for empty SCMS status response handling
dmccoystephenson Mar 27, 2026
de569bd
test: add tests for null and empty inputs in ScmsHealthMapper
dmccoystephenson Mar 27, 2026
ecbfc0e
test: ensure stable result when SCMS health records share identical t…
dmccoystephenson Mar 27, 2026
1eaeafc
refactor: extract reusable helper methods in ScmsHealthServiceTest to…
dmccoystephenson Mar 27, 2026
523cbf7
feat: update webapp to use new ISS SCMS status endpoint and update re…
dmccoystephenson Mar 31, 2026
6fc2c12
chore: remove legacy ISS SCMS status endpoint and associated tests, d…
dmccoystephenson Mar 31, 2026
b5b632a
feat: update IAPI SCMS health status handling to use string values ("…
dmccoystephenson Apr 1, 2026
a3e6ce8
feat: revert webapp SCMS status handling to use string values ("1"/"0…
dmccoystephenson Apr 1, 2026
032573e
test: update RsuMarker tests to use "scms" as displayType instead of …
dmccoystephenson Apr 1, 2026
17a93e4
feat: add ColumnTransformer for SCMS health field to handle type conv…
dmccoystephenson Apr 1, 2026
a2b5a3f
feat: add role-based authorization to ScmsHealthController, update te…
dmccoystephenson Apr 1, 2026
aa1d099
feat: update ScmsHealth query to ensure deterministic tie-breaking by…
dmccoystephenson Apr 1, 2026
a2175d5
test: expand unit tests for ScmsHealthService to validate edge cases …
dmccoystephenson Apr 2, 2026
3199062
test: add `@DisplayName` annotations to ScmsHealthControllerTest for …
dmccoystephenson Apr 2, 2026
9452d3f
test: add `@DisplayName` annotations to ScmsHealthServiceTest for imp…
dmccoystephenson Apr 2, 2026
1b9f2b9
feat: ensure deterministic tie-breaking for ScmsHealth query by refin…
dmccoystephenson Apr 2, 2026
5b9ec9b
test: add `@DisplayName` annotations to ScmsHealthMapperTest for impr…
dmccoystephenson Apr 2, 2026
69ef84a
feat: simplify ScmsHealth query by using MAX(id) for latest record se…
dmccoystephenson Apr 2, 2026
ee86517
docs: remove reference to deleted file
dmccoystephenson Apr 2, 2026
2cf7885
docs: update schema descriptions in ScmsHealthDto class
dmccoystephenson Apr 2, 2026
37ab283
docs: update SCMS health record descriptions in ScmsHealthDto class
dmccoystephenson Apr 2, 2026
195df88
feat: update ScmsHealth query to select the latest record by timestam…
dmccoystephenson Apr 2, 2026
1306349
docs: clarify SCMS health status schema description in ScmsHealthDto
dmccoystephenson Apr 2, 2026
625694c
docs: add `@Operation` annotation to ScmsHealthController.getAllStatu…
dmccoystephenson Apr 2, 2026
e87666a
refactor: replace `Rsu` entity with direct `InetAddress` reference in…
dmccoystephenson Apr 2, 2026
b05271a
Revert "feat: update ScmsHealth query to select the latest record by …
dmccoystephenson Apr 2, 2026
72655d3
feat(api): handle MissingRequestHeaderException with detailed error r…
mcook42 Mar 30, 2026
a89edd2
test: update expected error response for missing organization header …
dmccoystephenson Apr 2, 2026
50d481b
chore: restore dispatch of getIssScmsStatus action on WZDx marker sel…
dmccoystephenson Apr 2, 2026
ae68eea
refactor: extract zone ID as constant, update date time formatter in …
dmccoystephenson Apr 2, 2026
0dc42be
chore: update `findLatestScmsHealthByOrganization` to use Spring's `@…
dmccoystephenson Apr 2, 2026
10f1c99
Revert "test: update RsuMarker tests to use "scms" as displayType ins…
dmccoystephenson Apr 2, 2026
7b2420a
chore: remove unused Lombok @Slf4j annotation from ScmsHealthService
dmccoystephenson Apr 2, 2026
4db6f8a
chore: remove unnecessary newline in ScmsHealthRepository
dmccoystephenson Apr 2, 2026
a65d04f
refactor: simplify saveRsuWithoutOrg method by reusing saveRsu and ad…
dmccoystephenson Apr 2, 2026
496235f
chore: update SCMS status endpoint paths to `/scms/status` across API…
dmccoystephenson Apr 2, 2026
1f31192
refactor: replace getIssScmsStatus thunk with RTK Query-based scmsApi…
dmccoystephenson Apr 2, 2026
f8e3d80
feat: add configurable application timezone support with DateTimeConf…
dmccoystephenson Apr 3, 2026
bfebdc5
feat: add SCMS health sample data for RSUs in CVManager_SampleData.sql
dmccoystephenson Apr 3, 2026
f438cd1
refactor: remove unused Lombok annotations and update timezone except…
dmccoystephenson Apr 3, 2026
91a74f2
refactor: replace ScmsHealth subquery logic with ROW_NUMBER window fu…
dmccoystephenson Apr 3, 2026
234a87e
refactor: use ScmsHealthResponse wrapper for SCMS status API, update …
dmccoystephenson Apr 3, 2026
d6ca09d
chore: enforce compile error for unmapped fields in ScmsHealthMapper
dmccoystephenson Apr 3, 2026
2342613
chore: remove unused CVIZ_API_SERVER_URL environment variable from tests
dmccoystephenson Apr 3, 2026
28c97d4
docs: update response code order in API response documentation for SC…
dmccoystephenson Apr 3, 2026
6cf6d8e
chore: remove SCMS health data insertion from sample data SQL
dmccoystephenson Apr 3, 2026
061718f
refactor: update SCMS API base path to include `/devices` in controll…
dmccoystephenson Apr 8, 2026
3b93bf6
Merge branch 'refs/heads/develop' into feat/migrate-iss-scms-status-e…
dmccoystephenson Apr 10, 2026
6259b74
refactor: simplify SCMS health service tests by using TestFixtures fo…
dmccoystephenson Apr 10, 2026
0e02763
chore: adjust HikariCP settings in integration tests to manage Postgr…
dmccoystephenson Apr 10, 2026
70358f5
Merge remote-tracking branch 'origin/develop' into feat/migrate-iss-s…
drewjj Apr 17, 2026
0ce74c5
feat(tests): update ScmsHealthDtos to use Boolean for health values
mcook42 Apr 17, 2026
f567c8e
chore(tests): remove unused test ManufacturerRepository
mcook42 Apr 17, 2026
f9a525a
feat(api): remove DateTimeConfig and update SCMS expiration to Instant
mcook42 Apr 17, 2026
7a8765e
chore(env): remove APP_TIMEZONE configuration from env and compose files
mcook42 Apr 17, 2026
85da216
feat(webapp): improve SCMS expiration formatting for RSUs
mcook42 Apr 17, 2026
c989512
refactor(api): simplify `prepareHeaders` logic by removing unused end…
mcook42 Apr 17, 2026
035616e
refactor(webapp): replace `useLazyGetScmsStatusQuery` with `useGetScm…
mcook42 Apr 17, 2026
d79465d
fix(webapp): update SCMS status to use boolean type
mcook42 Apr 17, 2026
b5b0edb
Merge branch 'develop' of github.com:CDOT-CV/jpo-cvmanager into feat/…
mcook42 Apr 20, 2026
f60e865
test(tests): correct order of database cleanup and security context m…
mcook42 Apr 20, 2026
bc7ae98
chore(api): remove outdated comment from `ScmsHealthDto` definition
mcook42 Apr 20, 2026
5587cbd
test(webapp): enhance `RsuErrors` and `RsuFirmwareMenu` test coverage
mcook42 Apr 20, 2026
57e5ec6
test(webapp): update `DisplayRsuErrors` snapshot tests
mcook42 Apr 20, 2026
a9878cd
chore: move ScmsHealthRsuProjectionImpl to test package
mcook42 Apr 23, 2026
3bf5862
feat(sql): add index for `timestamp` column in `scms_health` table
mcook42 Apr 23, 2026
58e9b58
refactor(sql): move `scms_health` index creation to dedicated update …
mcook42 Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions resources/sql_scripts/update_scripts/scms_health_index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS idx_scms_health_timestamp
ON scms_health(timestamp);
76 changes: 0 additions & 76 deletions services/api/src/iss_scms_status.py

This file was deleted.

2 changes: 0 additions & 2 deletions services/api/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from rsu_geo_query import RsuGeoQuery
from wzdx_feed import WzdxFeed
from rsu_geo_msg_query import RsuGeoData
from iss_scms_status import IssScmsStatus
from rsu_ssm_srm import RsuSsmSrmData
from admin_new_user import AdminNewUser
from admin_new_org import AdminNewOrg
Expand Down Expand Up @@ -66,7 +65,6 @@ def apply_cors_header(response):
api.add_resource(RsuCommandRequest, "/rsu-command")
api.add_resource(RsuGeoQuery, "/rsu-config-geo-query")
api.add_resource(RsuGeoData, "/rsu-geo-msg-data")
api.add_resource(IssScmsStatus, "/iss-scms-status")
api.add_resource(RsuSsmSrmData, "/rsu-ssm-srm-data")
api.add_resource(RSUErrorSummaryResource, "/rsu-error-summary")
if api_environment.ENABLE_WZDX_FEATURES:
Expand Down
2 changes: 0 additions & 2 deletions services/api/src/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ def get_user_role(token) -> UserInfo | None:
"/rsucounts": True,
"/rsu-msgfwd-query": True,
"/rsu-command": True,
"/iss-scms-status": True,
"/wzdx-feed": False,
"/rsu-geo-msg-data": False,
"/rsu-ssm-srm-data": False,
Expand Down Expand Up @@ -86,7 +85,6 @@ def get_user_role(token) -> UserInfo | None:
"/rsu-msgfwd-query": FEATURE_KEYS_LITERAL.RSU,
"/rsu-command": FEATURE_KEYS_LITERAL.RSU,
"/rsu-map-info": FEATURE_KEYS_LITERAL.RSU,
"/iss-scms-status": FEATURE_KEYS_LITERAL.RSU,
"/wzdx-feed": FEATURE_KEYS_LITERAL.WZDX,
"/rsu-geo-msg-data": FEATURE_KEYS_LITERAL.RSU,
"/rsu-ssm-srm-data": FEATURE_KEYS_LITERAL.RSU,
Expand Down
51 changes: 0 additions & 51 deletions services/api/tests/data/iss_scms_status_data.py

This file was deleted.

101 changes: 0 additions & 101 deletions services/api/tests/src/test_iss_scms_status.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package us.dot.its.jpo.ode.api.controllers.scms;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import us.dot.its.jpo.ode.api.mappers.ScmsHealthMapper;
import us.dot.its.jpo.ode.api.models.scms.ScmsHealthResponse;
import us.dot.its.jpo.ode.api.services.ScmsHealthService;

@Slf4j
@RestController
@ConditionalOnProperty(name = "enable.api", havingValue = "true")
@RequestMapping("/devices/scms")
@RequiredArgsConstructor
@Tag(name = "SCMS Health Status", description = "Manage SCMS health status for RSUs")
public class ScmsHealthController {

private final ScmsHealthService scmsHealthService;
private final ScmsHealthMapper scmsHealthMapper;

@Operation(
summary = "Retrieve SCMS health status for RSUs in the given organization",
description = """
Returns a map of RSU IDs to their health status for the specified organization.
The Organization header is required for all users, including super users.
"""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(responseCode = "400", description = "Organization header is missing"),
@ApiResponse(responseCode = "403", description = "Forbidden - Requires SUPER_USER or USER role in the specified organization"),
@ApiResponse(responseCode = "404", description = "Organization not found"),
})
@GetMapping(value = "/status", produces = "application/json")
@PreAuthorize("@PermissionService.isSuperUser() || @PermissionService.hasRoleInOrg(#organization, 'USER')")
public ScmsHealthResponse getAllStatuses(@RequestHeader(name = "Organization") String organization) {
log.info("GET /devices/scms/status. organization: {}", organization);
return scmsHealthMapper.toResponse(scmsHealthService.getScmsStatuses(organization));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package us.dot.its.jpo.ode.api.mappers;

import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
import org.mapstruct.ReportingPolicy;
import us.dot.its.jpo.ode.api.models.postgres.projections.ScmsHealthRsuProjection;
import us.dot.its.jpo.ode.api.models.scms.ScmsHealthDto;
import us.dot.its.jpo.ode.api.models.scms.ScmsHealthResponse;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* MapStruct mapper for SCMS health data.
*
* <p>MapStruct generates {@link #toDto(ScmsHealthRsuProjection)} with compile-time field checking.
* If a field is added to {@link ScmsHealthDto} without a corresponding mapping, MapStruct emits
* a compile error (see {@code unmappedTargetPolicy}).</p>
*
* <p>{@link #toResponse(List)} is manually implemented because MapStruct does not yet support
* {@code List → Map} conversions keyed by a property. It delegates to the generated {@code toDto()}
* to preserve compile-time field checking.</p>
*
* <p>The {@code expiration} field is mapped directly as {@link java.time.Instant}. Jackson's
* {@code JavaTimeModule} (auto-registered by Spring Boot) serializes it as ISO-8601 UTC.</p>
*
* @see <a href="https://github.com/mapstruct/mapstruct/discussions/3263">MapStruct Discussion #3263</a>
* @see <a href="https://github.com/mapstruct/mapstruct/issues/3580">MapStruct Issue #3580</a>
*/
@Mapper(
componentModel = MappingConstants.ComponentModel.SPRING,
unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface ScmsHealthMapper {

/**
* Maps a single projection to DTO. MapStruct generates this method.
*/
ScmsHealthDto toDto(ScmsHealthRsuProjection projection);

/**
* Converts projections to a response keyed by IP address.
* Delegates to {@link #toDto} to preserve compile-time field checking.
*/
default ScmsHealthResponse toResponse(List<ScmsHealthRsuProjection> projections) {
if (projections == null) {
return null;
}
Map<String, ScmsHealthDto> scmsHealthByIp = new HashMap<>();
for (ScmsHealthRsuProjection projection : projections) {
String ip = projection.getIpv4Address().getHostAddress();
ScmsHealthDto dto = projection.getHealth() != null ? toDto(projection) : null;
scmsHealthByIp.put(ip, dto);
}
return new ScmsHealthResponse(scmsHealthByIp);
}
}
Loading
Loading