update
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 依赖于环境的 Maven 主目录路径
|
||||||
|
/mavenHomeManager.xml
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
10
.idea/fred-api.iml
generated
Normal file
10
.idea/fred-api.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/fred-api.iml" filepath="$PROJECT_DIR$/.idea/fred-api.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
550
handlers/fred.go
550
handlers/fred.go
@@ -43,25 +43,141 @@ func (h *FREDHandler) buildURL(endpoint string, params map[string]string) string
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) fetchData(url string, target interface{}) error {
|
func (h *FREDHandler) fetchData(target interface{}, c *gin.Context, endpoint string, params map[string]string) {
|
||||||
|
url := h.buildURL(endpoint, params)
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||||
|
ErrorCode: "NetworkError",
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||||
|
ErrorCode: "ReadError",
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
var errResp models.ErrorResponse
|
var errResp models.ErrorResponse
|
||||||
json.Unmarshal(body, &errResp)
|
json.Unmarshal(body, &errResp)
|
||||||
return fmt.Errorf("API error: %s - %s", errResp.ErrorCode, errResp.ErrorMessage)
|
c.JSON(http.StatusBadGateway, models.ErrorResponse{
|
||||||
|
ErrorCode: "APIError",
|
||||||
|
ErrorMessage: fmt.Sprintf("Status: %d, Error: %s - %s", resp.StatusCode, errResp.ErrorCode, errResp.ErrorMessage),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Unmarshal(body, target)
|
if err := json.Unmarshal(body, target); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||||
|
ErrorCode: "ParseError",
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) HealthCheck(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "FRED API",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategory(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.fetchData(&models.CategoryResponse{}, c, "/category", map[string]string{"category_id": categoryID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategoryChildren(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"category_id": categoryID}
|
||||||
|
h.fetchData(&models.CategoriesResponse{}, c, "/category/children", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategoryRelated(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.fetchData(&models.RelatedTagsResponse{}, c, "/category/related", map[string]string{"category_id": categoryID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategorySeries(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"category_id": categoryID}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.CategorySeriesResponse{}, c, "/category/series", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategoryTags(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"category_id": categoryID}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.CategoryTagsResponse{}, c, "/category/tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetCategoryRelatedTags(c *gin.Context) {
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
if categoryID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "category_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tagNames := c.Query("tag_names")
|
||||||
|
if tagNames == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "tag_names is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"category_id": categoryID, "tag_names": tagNames}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.RelatedTagsResponse{}, c, "/category/related_tags", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) GetSeries(c *gin.Context) {
|
func (h *FREDHandler) GetSeries(c *gin.Context) {
|
||||||
@@ -73,30 +189,23 @@ func (h *FREDHandler) GetSeries(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.SeriesResponse{}, c, "/series", params)
|
||||||
|
}
|
||||||
|
|
||||||
params := map[string]string{
|
func (h *FREDHandler) GetSeriesCategories(c *gin.Context) {
|
||||||
"series_id": seriesID,
|
seriesID := c.Query("series_id")
|
||||||
}
|
if seriesID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
if realtimeStart := c.Query("realtime_start"); realtimeStart != "" {
|
ErrorCode: "BadRequest",
|
||||||
params["realtime_start"] = realtimeStart
|
ErrorMessage: "series_id is required",
|
||||||
}
|
|
||||||
if realtimeEnd := c.Query("realtime_end"); realtimeEnd != "" {
|
|
||||||
params["realtime_end"] = realtimeEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
url := h.buildURL("/series", params)
|
|
||||||
|
|
||||||
var response models.SeriesListResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
c.JSON(http.StatusOK, response)
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.SeriesCategoriesResponse{}, c, "/series/categories", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) GetObservations(c *gin.Context) {
|
func (h *FREDHandler) GetObservations(c *gin.Context) {
|
||||||
@@ -108,35 +217,27 @@ func (h *FREDHandler) GetObservations(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
params := map[string]string{
|
h.addOptionalParams(c, params, []string{
|
||||||
"series_id": seriesID,
|
|
||||||
}
|
|
||||||
|
|
||||||
optionalParams := []string{
|
|
||||||
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
||||||
"observation_start", "observation_end", "units", "frequency",
|
"observation_start", "observation_end", "units", "frequency",
|
||||||
"aggregation_method", "output_type", "vintage_dates",
|
"aggregation_method", "output_type", "vintage_dates",
|
||||||
}
|
})
|
||||||
|
h.fetchData(&models.ObservationsResponse{}, c, "/series/observations", params)
|
||||||
|
}
|
||||||
|
|
||||||
for _, param := range optionalParams {
|
func (h *FREDHandler) GetSeriesRelease(c *gin.Context) {
|
||||||
if v := c.Query(param); v != "" {
|
seriesID := c.Query("series_id")
|
||||||
params[param] = v
|
if seriesID == "" {
|
||||||
}
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
}
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "series_id is required",
|
||||||
url := h.buildURL("/series/observations", params)
|
|
||||||
|
|
||||||
var response models.ObservationsResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
c.JSON(http.StatusOK, response)
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.ReleaseResponse{}, c, "/series/release", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) SearchSeries(c *gin.Context) {
|
func (h *FREDHandler) SearchSeries(c *gin.Context) {
|
||||||
@@ -148,90 +249,135 @@ func (h *FREDHandler) SearchSeries(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"search_text": searchText}
|
||||||
params := map[string]string{
|
h.addOptionalParams(c, params, []string{
|
||||||
"search_text": searchText,
|
|
||||||
}
|
|
||||||
|
|
||||||
optionalParams := []string{
|
|
||||||
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
||||||
"observation_start", "observation_end", "units", "frequency",
|
"order_by", "filter", "" +
|
||||||
"aggregation_method", "output_type",
|
"" +
|
||||||
}
|
"" +
|
||||||
|
"" +
|
||||||
|
"" +
|
||||||
|
"",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.SearchResponse{}, c, "/series/search", params)
|
||||||
|
}
|
||||||
|
|
||||||
for _, param := range optionalParams {
|
func (h *FREDHandler) SearchSeriesTags(c *gin.Context) {
|
||||||
if v := c.Query(param); v != "" {
|
searchText := c.Query("search_text")
|
||||||
params[param] = v
|
if searchText == "" {
|
||||||
}
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
}
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "search_text is required",
|
||||||
url := h.buildURL("/series/search", params)
|
|
||||||
|
|
||||||
var response models.SearchResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalServerError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
params := map[string]string{"search_text": searchText}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.TagsResponse{}, c, "/series/search/tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
func (h *FREDHandler) SearchSeriesRelatedTags(c *gin.Context) {
|
||||||
|
searchText := c.Query("search_text")
|
||||||
|
if searchText == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "search_text is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tagNames := c.Query("tag_names")
|
||||||
|
if tagNames == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "tag_names is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"search_text": searchText, "tag_names": tagNames}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.RelatedTagsResponse{}, c, "/series/search/related_tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSeriesTags(c *gin.Context) {
|
||||||
|
seriesID := c.Query("series_id")
|
||||||
|
if seriesID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "series_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.SeriesTagsResponse{}, c, "/series/tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSeriesUpdates(c *gin.Context) {
|
||||||
|
params := map[string]string{}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
|
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
||||||
|
"filter", "observation_start", "observation_end",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.SeriesUpdatesResponse{}, c, "/series/updates", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSeriesVintageDates(c *gin.Context) {
|
||||||
|
seriesID := c.Query("series_id")
|
||||||
|
if seriesID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "series_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"series_id": seriesID}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.VintageDatesResponse{}, c, "/series/vintagedates", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) GetReleases(c *gin.Context) {
|
func (h *FREDHandler) GetReleases(c *gin.Context) {
|
||||||
params := map[string]string{}
|
params := map[string]string{}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"})
|
||||||
optionalParams := []string{
|
h.fetchData(&models.ReleasesResponse{}, c, "/releases", params)
|
||||||
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, param := range optionalParams {
|
|
||||||
if v := c.Query(param); v != "" {
|
|
||||||
params[param] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url := h.buildURL("/releases", params)
|
|
||||||
|
|
||||||
var response models.ReleasesResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalServerError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) GetReleaseDates(c *gin.Context) {
|
func (h *FREDHandler) GetReleaseDates(c *gin.Context) {
|
||||||
params := map[string]string{}
|
params := map[string]string{}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
optionalParams := []string{
|
|
||||||
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
||||||
"release_id", "date", "start_date", "end_date",
|
"release_id", "date", "start_date", "end_date",
|
||||||
}
|
})
|
||||||
|
h.fetchData(&models.ReleaseDatesResponse{}, c, "/releases/dates", params)
|
||||||
|
}
|
||||||
|
|
||||||
for _, param := range optionalParams {
|
func (h *FREDHandler) GetRelease(c *gin.Context) {
|
||||||
if v := c.Query(param); v != "" {
|
releaseIDStr := c.Query("release_id")
|
||||||
params[param] = v
|
if releaseIDStr == "" {
|
||||||
}
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
}
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "release_id is required",
|
||||||
url := h.buildURL("/releases/dates", params)
|
|
||||||
|
|
||||||
var response models.ReleaseDatesResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalServerError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.ReleaseResponse{}, c, "/release", params)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
func (h *FREDHandler) GetReleaseDates2(c *gin.Context) {
|
||||||
|
releaseIDStr := c.Query("release_id")
|
||||||
|
if releaseIDStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "release_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order", "date", "start_date", "end_date"})
|
||||||
|
h.fetchData(&models.ReleaseDatesResponse{}, c, "/release/dates", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FREDHandler) GetReleaseSeries(c *gin.Context) {
|
func (h *FREDHandler) GetReleaseSeries(c *gin.Context) {
|
||||||
@@ -243,49 +389,171 @@ func (h *FREDHandler) GetReleaseSeries(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
|
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
||||||
|
"order_by", "filter", "observation_start", "observation_end", "units", "frequency",
|
||||||
|
"aggregation_method", "output_type",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.SearchResponse{}, c, "/release/series", params)
|
||||||
|
}
|
||||||
|
|
||||||
releaseID, err := strconv.Atoi(releaseIDStr)
|
func (h *FREDHandler) GetReleaseSources(c *gin.Context) {
|
||||||
if err != nil {
|
releaseIDStr := c.Query("release_id")
|
||||||
|
if releaseIDStr == "" {
|
||||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
ErrorCode: "BadRequest",
|
ErrorCode: "BadRequest",
|
||||||
ErrorMessage: "release_id must be a number",
|
ErrorMessage: "release_id is required",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.ReleaseSourcesResponse{}, c, "/release/sources", params)
|
||||||
|
}
|
||||||
|
|
||||||
params := map[string]string{
|
func (h *FREDHandler) GetReleaseTags(c *gin.Context) {
|
||||||
"release_id": strings.TrimSpace(strings.Replace(strconv.Itoa(releaseID), " ", "", -1)),
|
releaseIDStr := c.Query("release_id")
|
||||||
|
if releaseIDStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "release_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.ReleaseTagsResponse{}, c, "/release/tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
optionalParams := []string{
|
func (h *FREDHandler) GetReleaseRelatedTags(c *gin.Context) {
|
||||||
"realtime_start", "realtime_end", "limit", "offset", "sort_order",
|
releaseIDStr := c.Query("release_id")
|
||||||
"observation_start", "observation_end", "units", "frequency",
|
if releaseIDStr == "" {
|
||||||
"aggregation_method", "output_type",
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "release_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
tagNames := c.Query("tag_names")
|
||||||
|
if tagNames == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "tag_names is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID), "tag_names": tagNames}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"})
|
||||||
|
h.fetchData(&models.RelatedTagsResponse{}, c, "/release/related_tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
for _, param := range optionalParams {
|
func (h *FREDHandler) GetReleaseTables(c *gin.Context) {
|
||||||
|
releaseIDStr := c.Query("release_id")
|
||||||
|
if releaseIDStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "release_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
releaseID, _ := strconv.Atoi(releaseIDStr)
|
||||||
|
params := map[string]string{"release_id": strconv.Itoa(releaseID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.ReleaseTablesResponse{}, c, "/release/tables", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSources(c *gin.Context) {
|
||||||
|
params := map[string]string{}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"})
|
||||||
|
h.fetchData(&models.SourcesResponse{}, c, "/sources", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSource(c *gin.Context) {
|
||||||
|
sourceIDStr := c.Query("source_id")
|
||||||
|
if sourceIDStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "source_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceID, _ := strconv.Atoi(sourceIDStr)
|
||||||
|
params := map[string]string{"source_id": strconv.Itoa(sourceID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"})
|
||||||
|
h.fetchData(&models.SourceResponse{}, c, "/source", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetSourceReleases(c *gin.Context) {
|
||||||
|
sourceIDStr := c.Query("source_id")
|
||||||
|
if sourceIDStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "source_id is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceID, _ := strconv.Atoi(sourceIDStr)
|
||||||
|
params := map[string]string{"source_id": strconv.Itoa(sourceID)}
|
||||||
|
h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"})
|
||||||
|
h.fetchData(&models.ReleasesResponse{}, c, "/source/releases", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetTags(c *gin.Context) {
|
||||||
|
params := map[string]string{}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
|
"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order",
|
||||||
|
"tag_name", "tag_group_id", "filter",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.TagsResponse{}, c, "/tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetRelatedTags(c *gin.Context) {
|
||||||
|
tagNames := c.Query("tag_names")
|
||||||
|
if tagNames == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "tag_names is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"tag_names": tagNames}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
|
"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.RelatedTagsResponse{}, c, "/related_tags", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) GetTagsSeries(c *gin.Context) {
|
||||||
|
tagNames := c.Query("tag_names")
|
||||||
|
if tagNames == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
ErrorCode: "BadRequest",
|
||||||
|
ErrorMessage: "tag_names is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{"tag_names": tagNames}
|
||||||
|
h.addOptionalParams(c, params, []string{
|
||||||
|
"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order",
|
||||||
|
"filter", "" +
|
||||||
|
"" +
|
||||||
|
"" +
|
||||||
|
"" +
|
||||||
|
"" +
|
||||||
|
"",
|
||||||
|
})
|
||||||
|
h.fetchData(&models.SearchResponse{}, c, "/tags/series", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FREDHandler) addOptionalParams(c *gin.Context, params map[string]string, optionals []string) {
|
||||||
|
for _, param := range optionals {
|
||||||
if v := c.Query(param); v != "" {
|
if v := c.Query(param); v != "" {
|
||||||
params[param] = v
|
params[param] = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url := h.buildURL("/release/series", params)
|
|
||||||
|
|
||||||
var response models.SearchResponse
|
|
||||||
if err := h.fetchData(url, &response); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
|
||||||
ErrorCode: "InternalServerError",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *FREDHandler) HealthCheck(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "FRED API Proxy",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
42
main.go
42
main.go
@@ -41,25 +41,67 @@ func main() {
|
|||||||
|
|
||||||
api := router.Group("/api/v1")
|
api := router.Group("/api/v1")
|
||||||
{
|
{
|
||||||
|
api.GET("/category", fredHandler.GetCategory)
|
||||||
|
api.GET("/category/children", fredHandler.GetCategoryChildren)
|
||||||
|
api.GET("/category/related", fredHandler.GetCategoryRelated)
|
||||||
|
api.GET("/category/series", fredHandler.GetCategorySeries)
|
||||||
|
api.GET("/category/tags", fredHandler.GetCategoryTags)
|
||||||
|
api.GET("/category/related_tags", fredHandler.GetCategoryRelatedTags)
|
||||||
|
|
||||||
api.GET("/series", fredHandler.GetSeries)
|
api.GET("/series", fredHandler.GetSeries)
|
||||||
|
api.GET("/series/categories", fredHandler.GetSeriesCategories)
|
||||||
api.GET("/series/observations", fredHandler.GetObservations)
|
api.GET("/series/observations", fredHandler.GetObservations)
|
||||||
|
api.GET("/series/release", fredHandler.GetSeriesRelease)
|
||||||
api.GET("/series/search", fredHandler.SearchSeries)
|
api.GET("/series/search", fredHandler.SearchSeries)
|
||||||
|
api.GET("/series/search/tags", fredHandler.SearchSeriesTags)
|
||||||
|
api.GET("/series/search/related_tags", fredHandler.SearchSeriesRelatedTags)
|
||||||
|
api.GET("/series/tags", fredHandler.GetSeriesTags)
|
||||||
|
api.GET("/series/updates", fredHandler.GetSeriesUpdates)
|
||||||
|
api.GET("/series/vintagedates", fredHandler.GetSeriesVintageDates)
|
||||||
|
|
||||||
api.GET("/releases", fredHandler.GetReleases)
|
api.GET("/releases", fredHandler.GetReleases)
|
||||||
api.GET("/releases/dates", fredHandler.GetReleaseDates)
|
api.GET("/releases/dates", fredHandler.GetReleaseDates)
|
||||||
|
api.GET("/release", fredHandler.GetRelease)
|
||||||
|
api.GET("/release/dates", fredHandler.GetReleaseDates2)
|
||||||
api.GET("/release/series", fredHandler.GetReleaseSeries)
|
api.GET("/release/series", fredHandler.GetReleaseSeries)
|
||||||
|
api.GET("/release/sources", fredHandler.GetReleaseSources)
|
||||||
|
api.GET("/release/tags", fredHandler.GetReleaseTags)
|
||||||
|
api.GET("/release/related_tags", fredHandler.GetReleaseRelatedTags)
|
||||||
|
api.GET("/release/tables", fredHandler.GetReleaseTables)
|
||||||
|
|
||||||
|
api.GET("/sources", fredHandler.GetSources)
|
||||||
|
api.GET("/source", fredHandler.GetSource)
|
||||||
|
api.GET("/source/releases", fredHandler.GetSourceReleases)
|
||||||
|
|
||||||
|
api.GET("/tags", fredHandler.GetTags)
|
||||||
|
api.GET("/related_tags", fredHandler.GetRelatedTags)
|
||||||
|
api.GET("/tags/series", fredHandler.GetTagsSeries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.Static("/static", "./web/static")
|
||||||
|
router.LoadHTMLGlob("web/templates/*")
|
||||||
|
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index.html", gin.H{
|
||||||
|
"title": "FRED Economic Data Dashboard",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Printf("FRED API Server starting on :%s\n", cfg.ServerPort)
|
fmt.Printf("FRED API Server starting on :%s\n", cfg.ServerPort)
|
||||||
fmt.Printf("FRED API Base URL: %s\n", cfg.FREDBaseURL)
|
fmt.Printf("FRED API Base URL: %s\n", cfg.FREDBaseURL)
|
||||||
fmt.Println("\nAvailable endpoints:")
|
fmt.Println("\nAvailable endpoints:")
|
||||||
fmt.Println(" GET /health - Health check")
|
fmt.Println(" GET /health - Health check")
|
||||||
|
fmt.Println(" GET / - Web Dashboard")
|
||||||
fmt.Println(" GET /api/v1/series - Get series info")
|
fmt.Println(" GET /api/v1/series - Get series info")
|
||||||
fmt.Println(" GET /api/v1/series/observations - Get observations")
|
fmt.Println(" GET /api/v1/series/observations - Get observations")
|
||||||
fmt.Println(" GET /api/v1/series/search - Search series")
|
fmt.Println(" GET /api/v1/series/search - Search series")
|
||||||
fmt.Println(" GET /api/v1/releases - Get all releases")
|
fmt.Println(" GET /api/v1/releases - Get all releases")
|
||||||
fmt.Println(" GET /api/v1/releases/dates - Get release dates")
|
fmt.Println(" GET /api/v1/releases/dates - Get release dates")
|
||||||
fmt.Println(" GET /api/v1/release/series - Get series in release")
|
fmt.Println(" GET /api/v1/release/series - Get series in release")
|
||||||
|
fmt.Println(" GET /api/v1/category - Get category")
|
||||||
|
fmt.Println(" GET /api/v1/sources - Get sources")
|
||||||
|
fmt.Println(" GET /api/v1/tags - Get tags")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("Examples:")
|
fmt.Println("Examples:")
|
||||||
fmt.Println(" curl 'http://localhost:" + cfg.ServerPort + "/api/v1/series?series_id=GNPCA'")
|
fmt.Println(" curl 'http://localhost:" + cfg.ServerPort + "/api/v1/series?series_id=GNPCA'")
|
||||||
|
|||||||
163
models/models.go
163
models/models.go
@@ -1,5 +1,25 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ParentID int `json:"parent_id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Category *Category `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoriesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Categories []Category `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
type Series struct {
|
type Series struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
RealtimeStart string `json:"realtime_start"`
|
RealtimeStart string `json:"realtime_start"`
|
||||||
@@ -24,6 +44,12 @@ type SeriesListResponse struct {
|
|||||||
Seriess []Series `json:"seriess"`
|
Seriess []Series `json:"seriess"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SeriesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Series Series `json:"series"`
|
||||||
|
}
|
||||||
|
|
||||||
type Observation struct {
|
type Observation struct {
|
||||||
RealtimeStart string `json:"realtime_start"`
|
RealtimeStart string `json:"realtime_start"`
|
||||||
RealtimeEnd string `json:"realtime_end"`
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
@@ -53,7 +79,14 @@ type SearchResult struct {
|
|||||||
RealtimeEnd string `json:"realtime_end"`
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Frequency string `json:"frequency"`
|
Frequency string `json:"frequency"`
|
||||||
|
FrequencyShort string `json:"frequency_short"`
|
||||||
Units string `json:"units"`
|
Units string `json:"units"`
|
||||||
|
UnitsShort string `json:"units_short"`
|
||||||
|
SeasonalAdjustment string `json:"seasonal_adjustment"`
|
||||||
|
SeasonalAdjustmentShort string `json:"seasonal_adjustment_short"`
|
||||||
|
LastUpdated string `json:"last_updated"`
|
||||||
|
Popularity int `json:"popularity"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResponse struct {
|
type SearchResponse struct {
|
||||||
@@ -86,6 +119,12 @@ type ReleasesResponse struct {
|
|||||||
Releases []Release `json:"releases"`
|
Releases []Release `json:"releases"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReleaseResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Release Release `json:"release"`
|
||||||
|
}
|
||||||
|
|
||||||
type ReleaseDatesResponse struct {
|
type ReleaseDatesResponse struct {
|
||||||
RealtimeStart string `json:"realtime_start"`
|
RealtimeStart string `json:"realtime_start"`
|
||||||
RealtimeEnd string `json:"realtime_end"`
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
@@ -99,6 +138,130 @@ type ReleaseDatesResponse struct {
|
|||||||
} `json:"release_dates"`
|
} `json:"release_dates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourcesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Sources []Source `json:"sources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Source Source `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
GroupName string `json:"group_name"`
|
||||||
|
Serverity string `json:"serverity"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
Popularity int `json:"popularity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TagsResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelatedTagsResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
RelatedTags []Tag `json:"related_tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesUpdate struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Frequency string `json:"frequency"`
|
||||||
|
Units string `json:"units"`
|
||||||
|
LastUpdated string `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesUpdatesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Observations []SeriesUpdate `json:"seriess"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VintageDatesResponse struct {
|
||||||
|
VintageDates []string `json:"vintage_dates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategorySeriesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Series []Series `json:"seriess"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryTagsResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesTagsResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesCategoriesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Categories []Category `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseSourcesResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Sources []Source `json:"sources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseTagsResponse struct {
|
||||||
|
RealtimeStart string `json:"realtime_start"`
|
||||||
|
RealtimeEnd string `json:"realtime_end"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseTablesResponse struct {
|
||||||
|
Result struct {
|
||||||
|
Data []struct {
|
||||||
|
SeriesID string `json:"series_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
LastUpdated string `json:"last_updated"`
|
||||||
|
Frequency string `json:"frequency"`
|
||||||
|
Units string `json:"units"`
|
||||||
|
SeasonalAdj string `json:"seasonal_adjustment"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
ErrorCode string `json:"error_code"`
|
ErrorCode string `json:"error_code"`
|
||||||
ErrorMessage string `json:"error_message"`
|
ErrorMessage string `json:"error_message"`
|
||||||
|
|||||||
137
web/static/css/style.css
Normal file
137
web/static/css/style.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #1a73e8;
|
||||||
|
--secondary-color: #5f6368;
|
||||||
|
--success-color: #34a853;
|
||||||
|
--danger-color: #ea4335;
|
||||||
|
--warning-color: #fbbc04;
|
||||||
|
--info-color: #4285f4;
|
||||||
|
--dark-bg: #202124;
|
||||||
|
--light-bg: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-radius: 8px 8px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chart-placeholder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dataChart {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dataChart.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-width: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item:hover {
|
||||||
|
background-color: #e8f0fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item.active {
|
||||||
|
background-color: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item {
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item:hover {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-item {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-date {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-time {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-light:hover {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: #1a73e8;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(26, 115, 232, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
385
web/static/js/app.js
Normal file
385
web/static/js/app.js
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
const API_BASE = '/api/v1';
|
||||||
|
let chart = null;
|
||||||
|
let currentSeriesData = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
|
const popularSeries = ['GNPCA', 'GDP', 'UNRATE', 'CPIAUCSL', 'FEDFUNDS', 'M2SL', 'SP500', 'EXUSEU'];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
updateTime();
|
||||||
|
setInterval(updateTime, 1000);
|
||||||
|
loadPopularSeries();
|
||||||
|
loadCategories();
|
||||||
|
loadReleases();
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('current-time').textContent = now.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
const ctx = document.getElementById('dataChart').getContext('2d');
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Value',
|
||||||
|
data: [],
|
||||||
|
borderColor: '#1a73e8',
|
||||||
|
backgroundColor: 'rgba(26, 115, 232, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchSeries() {
|
||||||
|
const searchText = document.getElementById('search-input').value.trim();
|
||||||
|
if (!searchText) {
|
||||||
|
alert('Please enter a search term');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/series/search`, {
|
||||||
|
params: { search_text: searchText, limit: 20 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.seriess && response.data.seriess.length > 0) {
|
||||||
|
displaySearchResults(response.data.seriess);
|
||||||
|
} else {
|
||||||
|
alert('No results found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
alert('Search failed: ' + (error.response?.data?.error_message || error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySearchResults(results) {
|
||||||
|
const select = document.getElementById('series-select');
|
||||||
|
select.innerHTML = '<option value="">-- Search Results --</option>';
|
||||||
|
|
||||||
|
results.forEach(series => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = series.id;
|
||||||
|
option.textContent = `${series.id} - ${series.title}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
select.value = results[0].id;
|
||||||
|
loadSeriesData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPopularSeries() {
|
||||||
|
const select = document.getElementById('series-select');
|
||||||
|
select.innerHTML = '<option value="">-- Popular Series --</option>';
|
||||||
|
|
||||||
|
popularSeries.forEach(seriesId => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = seriesId;
|
||||||
|
option.textContent = seriesId;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (popularSeries.length > 0) {
|
||||||
|
select.value = popularSeries[0];
|
||||||
|
loadSeriesData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSeriesData() {
|
||||||
|
const seriesId = document.getElementById('series-select').value;
|
||||||
|
if (!seriesId) {
|
||||||
|
document.getElementById('chart-placeholder').style.display = 'block';
|
||||||
|
document.getElementById('dataChart').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [seriesRes, obsRes] = await Promise.all([
|
||||||
|
axios.get(`${API_BASE}/series`, { params: { series_id: seriesId } }),
|
||||||
|
axios.get(`${API_BASE}/series/observations`, { params: { series_id: seriesId, limit: 1000 } })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const series = seriesRes.data.series;
|
||||||
|
const observations = obsRes.data.observations || [];
|
||||||
|
|
||||||
|
displaySeriesInfo(series);
|
||||||
|
displayChart(observations, seriesId);
|
||||||
|
currentSeriesData = observations;
|
||||||
|
renderTable(1);
|
||||||
|
updatePagination();
|
||||||
|
|
||||||
|
document.getElementById('chart-placeholder').style.display = 'none';
|
||||||
|
document.getElementById('dataChart').style.display = 'block';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load series error:', error);
|
||||||
|
alert('Failed to load series: ' + (error.response?.data?.error_message || error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySeriesInfo(series) {
|
||||||
|
const info = document.getElementById('series-info');
|
||||||
|
info.innerHTML = `
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Series ID:</strong> ${series.id}</p>
|
||||||
|
<p><strong>Title:</strong> ${series.title}</p>
|
||||||
|
<p><strong>Frequency:</strong> ${series.frequency}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Units:</strong> ${series.units}</p>
|
||||||
|
<p><strong>Seasonal Adjustment:</strong> ${series.seasonal_adjustment}</p>
|
||||||
|
<p><strong>Last Updated:</strong> ${series.last_updated}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${series.notes ? `<p class="mt-2"><strong>Notes:</strong> ${series.notes}</p>` : ''}
|
||||||
|
`;
|
||||||
|
document.getElementById('chart-title').textContent = series.title || series.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayChart(observations, seriesId) {
|
||||||
|
const labels = observations.map(o => o.date).reverse();
|
||||||
|
const data = observations.map(o => parseFloat(o.value) || null).reverse();
|
||||||
|
|
||||||
|
chart.data.labels = labels;
|
||||||
|
chart.data.datasets[0].data = data;
|
||||||
|
chart.data.datasets[0].label = seriesId;
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(page) {
|
||||||
|
currentPage = page;
|
||||||
|
const start = (page - 1) * itemsPerPage;
|
||||||
|
const end = start + itemsPerPage;
|
||||||
|
const pageData = currentSeriesData.slice(start, end);
|
||||||
|
|
||||||
|
const tbody = document.getElementById('data-table-body');
|
||||||
|
if (pageData.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="2" class="text-center text-muted">No data available</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = pageData.map(obs => `
|
||||||
|
<tr>
|
||||||
|
<td>${obs.date}</td>
|
||||||
|
<td>${obs.value || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination() {
|
||||||
|
const totalPages = Math.ceil(currentSeriesData.length / itemsPerPage);
|
||||||
|
const pagination = document.getElementById('pagination');
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (currentPage > 1) {
|
||||||
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${currentPage - 1})">Prev</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
if (i === currentPage) {
|
||||||
|
html += `<li class="page-item active"><a class="page-link" href="#">${i}</a></li>`;
|
||||||
|
} else if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
|
||||||
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${i})">${i}</a></li>`;
|
||||||
|
} else if (i === currentPage - 3 || i === currentPage + 3) {
|
||||||
|
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${currentPage + 1})">Next</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/category/children`, {
|
||||||
|
params: { category_id: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = document.getElementById('categories-list');
|
||||||
|
if (response.data && response.data.categories) {
|
||||||
|
list.innerHTML = response.data.categories.map(cat => `
|
||||||
|
<div class="category-item" onclick="loadCategorySeries(${cat.id}, '${cat.name}')">
|
||||||
|
${cat.name}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load categories error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCategorySeries(categoryId, categoryName) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/category/series`, {
|
||||||
|
params: { category_id: categoryId, limit: 10 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = document.getElementById('series-select');
|
||||||
|
select.innerHTML = `<option value="">-- ${categoryName} --</option>`;
|
||||||
|
|
||||||
|
if (response.data && response.data.seriess) {
|
||||||
|
response.data.seriess.forEach(series => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = series.id;
|
||||||
|
option.textContent = `${series.id} - ${series.title}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.seriess.length > 0) {
|
||||||
|
select.value = response.data.seriess[0].id;
|
||||||
|
loadSeriesData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load category series error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReleases() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/releases/dates`, {
|
||||||
|
params: { limit: 10, start_date: getDateDaysAgo(30) }
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = document.getElementById('releases-list');
|
||||||
|
if (response.data && response.data.release_dates) {
|
||||||
|
list.innerHTML = response.data.release_dates.map(rel => `
|
||||||
|
<div class="release-item" onclick="loadReleaseSeries(${rel.release_id}, '${rel.release_name}')">
|
||||||
|
<div class="fw-bold">${rel.release_name}</div>
|
||||||
|
<div class="release-date">${rel.date}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load releases error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReleaseSeries(releaseId, releaseName) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/release/series`, {
|
||||||
|
params: { release_id: releaseId, limit: 10 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = document.getElementById('series-select');
|
||||||
|
select.innerHTML = `<option value="">-- ${releaseName} --</option>`;
|
||||||
|
|
||||||
|
if (response.data && response.data.seriess) {
|
||||||
|
response.data.seriess.forEach(series => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = series.id;
|
||||||
|
option.textContent = `${series.id} - ${series.title}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.seriess.length > 0) {
|
||||||
|
select.value = response.data.seriess[0].id;
|
||||||
|
loadSeriesData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load release series error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSeriesModal() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('addSeriesModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSeriesFromModal() {
|
||||||
|
const seriesId = document.getElementById('modal-series-id').value.trim();
|
||||||
|
if (!seriesId) {
|
||||||
|
alert('Please enter a series ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('series-select').value = seriesId;
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('addSeriesModal')).hide();
|
||||||
|
loadSeriesData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportData() {
|
||||||
|
if (!currentSeriesData.length) {
|
||||||
|
alert('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let csv = 'Date,Value\n';
|
||||||
|
currentSeriesData.forEach(obs => {
|
||||||
|
csv += `${obs.date},${obs.value}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `fred_data_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateDaysAgo(days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - days);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
document.getElementById('search-input').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
searchSeries();
|
||||||
|
}
|
||||||
|
});
|
||||||
183
web/templates/index.html
Normal file
183
web/templates/index.html
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FRED Economic Data Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<i class="bi bi-graph-up-arrow me-2"></i>
|
||||||
|
FRED Economic Data Dashboard
|
||||||
|
</a>
|
||||||
|
<span class="navbar-text text-light" id="current-time"></span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-search me-2"></i>Search</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" id="search-input" placeholder="Search series (e.g., GDP)">
|
||||||
|
<button class="btn btn-primary" onclick="searchSeries()">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-secondary" onclick="loadPopularSeries()">
|
||||||
|
<i class="bi bi-star me-2"></i>Popular Series
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-collection me-2"></i>Categories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="categories-list" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
<span class="ms-2">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-newspaper me-2"></i>Recent Releases</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="releases-list" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
<span class="ms-2">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0" id="chart-title">Select a series to view data</h5>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-light" onclick="exportData()" title="Export Data">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="series-selector" class="mb-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<select class="form-select" id="series-select" onchange="loadSeriesData()">
|
||||||
|
<option value="">-- Select a Series --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button class="btn btn-primary w-100" onclick="showSeriesModal()">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>Add Series
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="height: 400px;">
|
||||||
|
<canvas id="dataChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="chart-placeholder" class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-graph-up" style="font-size: 4rem;"></i>
|
||||||
|
<p class="mt-3">Select or search for a series to visualize data</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-table me-2"></i>Data Table</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<table class="table table-striped table-hover" id="data-table">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="data-table-body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center text-muted">No data available</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination justify-content-center" id="pagination"></ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Series Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="series-info">
|
||||||
|
<p class="text-muted">Select a series to view details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addSeriesModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Series</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Series ID</label>
|
||||||
|
<input type="text" class="form-control" id="modal-series-id" placeholder="e.g., GNPCA, GDP">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Observation Limit</label>
|
||||||
|
<select class="form-select" id="modal-limit">
|
||||||
|
<option value="10">Last 10</option>
|
||||||
|
<option value="50" selected>Last 50</option>
|
||||||
|
<option value="100">Last 100</option>
|
||||||
|
<option value="1000">All</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="addSeriesFromModal()">Load</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="bg-dark text-white text-center py-3 mt-4">
|
||||||
|
<p class="mb-0">FRED Economic Data Dashboard | Data provided by Federal Reserve Bank of St. Louis</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user