From 11fad22ea93548330802ca47ec1339e6a4f183cc Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:46:21 -0500 Subject: [PATCH 1/7] feat(db): add meal groups support for staggered meal times - Add meal_group column to applications table - Initialize default meal_groups in settings table --- cmd/migrate/migrations/000023_add_meal_groups.down.sql | 5 +++++ cmd/migrate/migrations/000023_add_meal_groups.up.sql | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 cmd/migrate/migrations/000023_add_meal_groups.down.sql create mode 100644 cmd/migrate/migrations/000023_add_meal_groups.up.sql diff --git a/cmd/migrate/migrations/000023_add_meal_groups.down.sql b/cmd/migrate/migrations/000023_add_meal_groups.down.sql new file mode 100644 index 00000000..d78070d6 --- /dev/null +++ b/cmd/migrate/migrations/000023_add_meal_groups.down.sql @@ -0,0 +1,5 @@ +-- Remove the meal_groups setting +DELETE FROM settings WHERE key = 'meal_groups'; + +-- Remove the meal_group column from applications +ALTER TABLE applications DROP COLUMN meal_group; diff --git a/cmd/migrate/migrations/000023_add_meal_groups.up.sql b/cmd/migrate/migrations/000023_add_meal_groups.up.sql new file mode 100644 index 00000000..017c5d34 --- /dev/null +++ b/cmd/migrate/migrations/000023_add_meal_groups.up.sql @@ -0,0 +1,8 @@ +-- Add nullable meal_group column to applications +ALTER TABLE applications ADD COLUMN meal_group TEXT; + +-- Add default meal groups to settings +-- Assumes a settings table with 'key' and 'value' (JSONB) columns +INSERT INTO settings (key, value) +VALUES ('meal_groups', '["A", "B", "C", "D"]'::jsonb) +ON CONFLICT (key) DO NOTHING; From 5721ac1142d8011e694e7e1b52e0acef47ef5175 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:07:20 -0500 Subject: [PATCH 2/7] feat(api): implement meal group support for staggered hacker check-in - Add meal_group column to applications and default configuration in settings - Implement store methods for meal group management and distribution stats - Add superadmin endpoints to GET/PUT meal groups and view assignment counts - Automate random meal group assignment during the check-in scan process - Include meal_group information in the scan creation response - Update application retrieval queries to include the meal_group field --- cmd/api/api.go | 3 + cmd/api/scans.go | 54 ++++++- cmd/api/scans_test.go | 51 +++++++ cmd/api/settings.go | 107 ++++++++++++++ cmd/api/settings_test.go | 84 +++++++++++ docs/docs.go | 271 ++++++++++++++++++++++++++++++++++- internal/store/mock_store.go | 34 +++++ internal/store/settings.go | 80 +++++++++++ internal/store/storage.go | 5 + 9 files changed, 686 insertions(+), 3 deletions(-) diff --git a/cmd/api/api.go b/cmd/api/api.go index e48d1ed8..87035d68 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -238,6 +238,9 @@ func (app *application) mount() http.Handler { r.Get("/hackathon-date-range", app.getHackathonDateRange) r.Post("/hackathon-date-range", app.setHackathonDateRange) r.Put("/scan-types", app.updateScanTypesHandler) + r.Get("/meal-groups", app.getMealGroups) + r.Put("/meal-groups", app.updateMealGroups) + r.Get("/meal-groups/stats", app.getMealGroupStats) }) r.Route("/applications", func(r chi.Router) { diff --git a/cmd/api/scans.go b/cmd/api/scans.go index 62064c20..54dcae87 100644 --- a/cmd/api/scans.go +++ b/cmd/api/scans.go @@ -1,7 +1,9 @@ package main import ( + "context" "errors" + "math/rand" "net/http" "github.com/go-chi/chi" @@ -29,6 +31,11 @@ type UpdateScanTypesPayload struct { ScanTypes []store.ScanType `json:"scan_types" validate:"required,dive"` } +type CreateScanResponse struct { + *store.Scan + MealGroup *string `json:"meal_group"` +} + // getScanTypesHandler returns all configured scan types // // @Summary Get scan types (Admin) @@ -61,7 +68,7 @@ func (app *application) getScanTypesHandler(w http.ResponseWriter, r *http.Reque // @Accept json // @Produce json // @Param scan body CreateScanPayload true "Scan to create" -// @Success 201 {object} store.Scan +// @Success 201 {object} CreateScanResponse // @Failure 400 {object} object{error=string} // @Failure 401 {object} object{error=string} // @Failure 403 {object} object{error=string} @@ -148,7 +155,23 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request return } - if err := app.jsonResponse(w, http.StatusCreated, scan); err != nil { + if found.Category == store.ScanCategoryCheckIn { + app.assignMealGroup(r.Context(), req.UserID) + } + + // Fetch meal group for response (non-fatal) + mealGroup, err := app.store.Application.GetMealGroupByUserID(r.Context(), req.UserID) + if err != nil && !errors.Is(err, store.ErrNotFound) { + // We don't want to fail the scan if we can't get the meal group info + app.logger.Warnw("failed to fetch meal group for scan response", "user_id", req.UserID, "error", err) + } + + response := CreateScanResponse{ + Scan: scan, + MealGroup: mealGroup, + } + + if err := app.jsonResponse(w, http.StatusCreated, response); err != nil { app.internalServerError(w, r, err) } } @@ -185,6 +208,33 @@ func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Reque } } +func (app *application) assignMealGroup(ctx context.Context, userID string) { + groups, err := app.store.Settings.GetMealGroups(ctx) + if err != nil { + app.logger.Warnw("failed to fetch meal groups for assignment", "error", err) + return + } + + if len(groups) == 0 { + return + } + + hackerApp, err := app.store.Application.GetByUserID(ctx, userID) + if err != nil { + app.logger.Warnw("failed to fetch application for meal group assignment", "user_id", userID, "error", err) + return + } + + if hackerApp.MealGroup != nil { + return // Already assigned + } + + selectedGroup := groups[rand.Intn(len(groups))] + if err := app.store.Application.SetMealGroup(ctx, hackerApp.ID, selectedGroup); err != nil { + app.logger.Warnw("failed to set meal group on application", "app_id", hackerApp.ID, "error", err) + } +} + // getScanStatsHandler returns aggregate scan counts grouped by scan type // // @Summary Get scan statistics (Admin) diff --git a/cmd/api/scans_test.go b/cmd/api/scans_test.go index 61f7487e..38513d50 100644 --- a/cmd/api/scans_test.go +++ b/cmd/api/scans_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "encoding/json" "net/http" "strings" @@ -56,8 +57,16 @@ func TestCreateScan(t *testing.T) { app := newTestApplication(t) mockSettings := app.store.Settings.(*store.MockSettingsStore) mockScans := app.store.Scans.(*store.MockScansStore) + mockApps := app.store.Application.(*store.MockApplicationStore) + + groups := []string{"A", "B"} + hackerApp := &store.Application{ID: "app-1", UserID: "user-1", MealGroup: nil} mockSettings.On("GetScanTypes").Return(scanTypes, nil).Once() + mockSettings.On("GetMealGroups").Return(groups, nil).Once() + mockApps.On("GetByUserID", "user-1").Return(hackerApp, nil).Once() + mockApps.On("SetMealGroup", "app-1", mock.AnythingOfType("string")).Return(nil).Once() + mockApps.On("GetMealGroupByUserID", "user-1").Return(&groups[0], nil).Once() mockScans.On("Create", mock.AnythingOfType("*store.Scan")).Return(nil).Once() body := `{"user_id":"user-1","scan_type":"check_in"}` @@ -69,8 +78,50 @@ func TestCreateScan(t *testing.T) { rr := executeRequest(req, http.HandlerFunc(app.createScanHandler)) checkResponseCode(t, http.StatusCreated, rr.Code) + var resp struct { + Data CreateScanResponse `json:"data"` + } + err = json.NewDecoder(rr.Body).Decode(&resp) + require.NoError(t, err) + assert.NotNil(t, resp.Data.MealGroup) + assert.Equal(t, groups[0], *resp.Data.MealGroup) + + mockSettings.AssertExpectations(t) + mockScans.AssertExpectations(t) + mockApps.AssertExpectations(t) + }) + + t.Run("check_in success - meal group assignment failure is non-fatal", func(t *testing.T) { + app := newTestApplication(t) + mockSettings := app.store.Settings.(*store.MockSettingsStore) + mockScans := app.store.Scans.(*store.MockScansStore) + mockApps := app.store.Application.(*store.MockApplicationStore) + + mockSettings.On("GetScanTypes").Return(scanTypes, nil).Once() + // Simulate error in meal group fetching + mockSettings.On("GetMealGroups").Return(nil, errors.New("db error")).Once() + mockApps.On("GetMealGroupByUserID", "user-1").Return(nil, store.ErrNotFound).Once() + mockScans.On("Create", mock.AnythingOfType("*store.Scan")).Return(nil).Once() + + body := `{"user_id":"user-1","scan_type":"check_in"}` + req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req = setUserContext(req, newAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.createScanHandler)) + checkResponseCode(t, http.StatusCreated, rr.Code) + + var resp struct { + Data CreateScanResponse `json:"data"` + } + err = json.NewDecoder(rr.Body).Decode(&resp) + require.NoError(t, err) + assert.Nil(t, resp.Data.MealGroup) + mockSettings.AssertExpectations(t) mockScans.AssertExpectations(t) + mockApps.AssertExpectations(t) }) t.Run("item scan when checked in", func(t *testing.T) { diff --git a/cmd/api/settings.go b/cmd/api/settings.go index 4a4163de..fa769d54 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -414,3 +414,110 @@ func (app *application) setHackathonDateRange(w http.ResponseWriter, r *http.Req app.internalServerError(w, r, err) } } + +type UpdateMealGroupsPayload struct { + Groups []string `json:"groups" validate:"required,min=1,dive,required,min=1,max=50"` +} + +type MealGroupsResponse struct { + Groups []string `json:"groups"` +} + +type MealGroupStatsResponse struct { + Stats map[string]int `json:"stats"` +} + +// getMealGroups returns the configured meal group names +// +// @Summary Get meal groups (Super Admin) +// @Description Returns the configured list of meal group names +// @Tags superadmin/settings +// @Produce json +// @Success 200 {object} MealGroupsResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/settings/meal-groups [get] +func (app *application) getMealGroups(w http.ResponseWriter, r *http.Request) { + groups, err := app.store.Settings.GetMealGroups(r.Context()) + if err != nil { + app.internalServerError(w, r, err) + return + } + + if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse{Groups: groups}); err != nil { + app.internalServerError(w, r, err) + } +} + +// updateMealGroups replaces all meal group names +// +// @Summary Update meal groups (Super Admin) +// @Description Replaces the available meal group names with the provided array +// @Tags superadmin/settings +// @Accept json +// @Produce json +// @Param groups body UpdateMealGroupsPayload true "Groups to set" +// @Success 200 {object} MealGroupsResponse +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/settings/meal-groups [put] +func (app *application) updateMealGroups(w http.ResponseWriter, r *http.Request) { + var req UpdateMealGroupsPayload + if err := readJSON(w, r, &req); err != nil { + app.badRequestResponse(w, r, err) + return + } + + if err := Validate.Struct(req); err != nil { + app.badRequestResponse(w, r, err) + return + } + + // Validate unique names + nameMap := make(map[string]bool) + for _, name := range req.Groups { + if nameMap[name] { + app.badRequestResponse(w, r, errors.New("duplicate meal group name: "+name)) + return + } + nameMap[name] = true + } + + if err := app.store.Settings.SetMealGroups(r.Context(), req.Groups); err != nil { + app.internalServerError(w, r, err) + return + } + + if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse(req)); err != nil { + app.internalServerError(w, r, err) + } +} + +// getMealGroupStats returns the number of hackers assigned to each meal group +// +// @Summary Get meal group stats (Super Admin) +// @Description Returns assignment counts for each configured meal group +// @Tags superadmin/settings +// @Produce json +// @Success 200 {object} MealGroupStatsResponse +// @Failure 401 {object} object{error=string} +// @Failure 403 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Security CookieAuth +// @Router /superadmin/settings/meal-groups/stats [get] +func (app *application) getMealGroupStats(w http.ResponseWriter, r *http.Request) { + stats, err := app.store.Settings.GetMealGroupStats(r.Context()) + if err != nil { + app.internalServerError(w, r, err) + return + } + + if err := app.jsonResponse(w, http.StatusOK, MealGroupStatsResponse{Stats: stats}); err != nil { + app.internalServerError(w, r, err) + } +} diff --git a/cmd/api/settings_test.go b/cmd/api/settings_test.go index 6e5b1eac..9377b760 100644 --- a/cmd/api/settings_test.go +++ b/cmd/api/settings_test.go @@ -426,3 +426,87 @@ func TestSetHackathonDateRange(t *testing.T) { checkResponseCode(t, http.StatusBadRequest, rr.Code) }) } + +func TestGetMealGroups(t *testing.T) { + app := newTestApplication(t) + mockSettings := app.store.Settings.(*store.MockSettingsStore) + + t.Run("should return meal groups", func(t *testing.T) { + groups := []string{"A", "B", "C"} + mockSettings.On("GetMealGroups").Return(groups, nil).Once() + + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.getMealGroups)) + checkResponseCode(t, http.StatusOK, rr.Code) + + var body struct { + Data MealGroupsResponse `json:"data"` + } + err = json.NewDecoder(rr.Body).Decode(&body) + require.NoError(t, err) + assert.Equal(t, groups, body.Data.Groups) + + mockSettings.AssertExpectations(t) + }) +} + +func TestUpdateMealGroups(t *testing.T) { + app := newTestApplication(t) + mockSettings := app.store.Settings.(*store.MockSettingsStore) + + t.Run("should update meal groups", func(t *testing.T) { + groups := []string{"Alpha", "Beta"} + mockSettings.On("SetMealGroups", groups).Return(nil).Once() + + body := `{"groups":["Alpha", "Beta"]}` + req, err := http.NewRequest(http.MethodPut, "/", strings.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.updateMealGroups)) + checkResponseCode(t, http.StatusOK, rr.Code) + + mockSettings.AssertExpectations(t) + }) + + t.Run("should return 400 for duplicate names", func(t *testing.T) { + body := `{"groups":["A", "A"]}` + req, err := http.NewRequest(http.MethodPut, "/", strings.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.updateMealGroups)) + checkResponseCode(t, http.StatusBadRequest, rr.Code) + }) +} + +func TestGetMealGroupStats(t *testing.T) { + app := newTestApplication(t) + mockSettings := app.store.Settings.(*store.MockSettingsStore) + + t.Run("should return stats", func(t *testing.T) { + stats := map[string]int{"A": 10, "B": 20} + mockSettings.On("GetMealGroupStats").Return(stats, nil).Once() + + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + req = setUserContext(req, newSuperAdminUser()) + + rr := executeRequest(req, http.HandlerFunc(app.getMealGroupStats)) + checkResponseCode(t, http.StatusOK, rr.Code) + + var body struct { + Data MealGroupStatsResponse `json:"data"` + } + err = json.NewDecoder(rr.Body).Decode(&body) + require.NoError(t, err) + assert.Equal(t, stats, body.Data.Stats) + + mockSettings.AssertExpectations(t) + }) +} diff --git a/docs/docs.go b/docs/docs.go index 9ee77088..57606f75 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -859,7 +859,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/store.Scan" + "$ref": "#/definitions/main.CreateScanResponse" } }, "400": { @@ -2505,6 +2505,203 @@ const docTemplate = `{ } } }, + "/superadmin/settings/meal-groups": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns the configured list of meal group names", + "produces": [ + "application/json" + ], + "tags": [ + "superadmin/settings" + ], + "summary": "Get meal groups (Super Admin)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.MealGroupsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "put": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Replaces the available meal group names with the provided array", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "superadmin/settings" + ], + "summary": "Update meal groups (Super Admin)", + "parameters": [ + { + "description": "Groups to set", + "name": "groups", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.UpdateMealGroupsPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.MealGroupsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, + "/superadmin/settings/meal-groups/stats": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns assignment counts for each configured meal group", + "produces": [ + "application/json" + ], + "tags": [ + "superadmin/settings" + ], + "summary": "Get meal group stats (Super Admin)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.MealGroupStatsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, "/superadmin/settings/review-assignment-toggle": { "put": { "security": [ @@ -3260,6 +3457,9 @@ const docTemplate = `{ "type": "string", "minLength": 1 }, + "meal_group": { + "type": "string" + }, "opt_in_mlh_emails": { "type": "boolean" }, @@ -3363,6 +3563,32 @@ const docTemplate = `{ } } }, + "main.CreateScanResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "meal_group": { + "type": "string" + }, + "scan_type": { + "type": "string" + }, + "scanned_at": { + "type": "string" + }, + "scanned_by": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "main.CreateSchedulePayload": { "type": "object", "required": [ @@ -3424,6 +3650,28 @@ const docTemplate = `{ } } }, + "main.MealGroupStatsResponse": { + "type": "object", + "properties": { + "stats": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, + "main.MealGroupsResponse": { + "type": "object", + "properties": { + "groups": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "main.NotesListResponse": { "type": "object", "properties": { @@ -3662,6 +3910,21 @@ const docTemplate = `{ "main.UpdateApplicationPayload": { "type": "object" }, + "main.UpdateMealGroupsPayload": { + "type": "object", + "required": [ + "groups" + ], + "properties": { + "groups": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, "main.UpdateRolePayload": { "type": "object", "required": [ @@ -3860,6 +4123,9 @@ const docTemplate = `{ "type": "string", "minLength": 1 }, + "meal_group": { + "type": "string" + }, "opt_in_mlh_emails": { "type": "boolean" }, @@ -3965,6 +4231,9 @@ const docTemplate = `{ "major": { "type": "string" }, + "meal_group": { + "type": "string" + }, "phone_e164": { "type": "string" }, diff --git a/internal/store/mock_store.go b/internal/store/mock_store.go index c01e027e..3ebfb72f 100644 --- a/internal/store/mock_store.go +++ b/internal/store/mock_store.go @@ -145,6 +145,19 @@ func (m *MockApplicationStore) GetEmailsByStatus(ctx context.Context, status App return args.Get(0).([]UserEmailInfo), args.Error(1) } +func (m *MockApplicationStore) SetMealGroup(ctx context.Context, id string, mealGroup string) error { + args := m.Called(id, mealGroup) + return args.Error(0) +} + +func (m *MockApplicationStore) GetMealGroupByUserID(ctx context.Context, userID string) (*string, error) { + args := m.Called(userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*string), args.Error(1) +} + // mock implementation of the Settings interface type MockSettingsStore struct { mock.Mock @@ -235,6 +248,27 @@ func (m *MockSettingsStore) GetScanStats(ctx context.Context) (map[string]int, e return args.Get(0).(map[string]int), args.Error(1) } +func (m *MockSettingsStore) GetMealGroups(ctx context.Context) ([]string, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} + +func (m *MockSettingsStore) SetMealGroups(ctx context.Context, groups []string) error { + args := m.Called(groups) + return args.Error(0) +} + +func (m *MockSettingsStore) GetMealGroupStats(ctx context.Context) (map[string]int, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]int), args.Error(1) +} + // MockApplicationReviewsStore is a mock implementation of the ApplicationReviews interface type MockApplicationReviewsStore struct { mock.Mock diff --git a/internal/store/settings.go b/internal/store/settings.go index a47ce9f5..40244960 100644 --- a/internal/store/settings.go +++ b/internal/store/settings.go @@ -27,6 +27,7 @@ const SettingsKeyScanTypes = "scan_types" const SettingsKeyScanStats = "scan_stats" const SettingsKeyAdminScheduleEditEnabled = "admin_schedule_edit_enabled" const SettingsKeyHackathonDateRange = "hackathon_date_range" +const SettingsKeyMealGroups = "meal_groups" type HackathonDateRange struct { StartDate *string `json:"start_date"` @@ -486,3 +487,82 @@ func (s *SettingsStore) SetHackathonDateRange(ctx context.Context, dateRange Hac _, err = s.db.ExecContext(ctx, query, SettingsKeyHackathonDateRange, string(jsonValue)) return err } + +// GetMealGroups returns the configured list of meal group names (e.g., ["A", "B", "C", "D"]) +func (s *SettingsStore) GetMealGroups(ctx context.Context) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := ` + SELECT value + FROM settings + WHERE key = $1 + ` + + var value []byte + err := s.db.QueryRowContext(ctx, query, SettingsKeyMealGroups).Scan(&value) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return []string{}, nil + } + return nil, err + } + + var groups []string + if err := json.Unmarshal(value, &groups); err != nil { + return nil, err + } + + return groups, nil +} + +// SetMealGroups updates the available meal group names +func (s *SettingsStore) SetMealGroups(ctx context.Context, groups []string) error { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + value, err := json.Marshal(groups) + if err != nil { + return err + } + + query := ` + INSERT INTO settings (key, value) + VALUES ($1, $2) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW() + ` + + _, err = s.db.ExecContext(ctx, query, SettingsKeyMealGroups, value) + return err +} + +// GetMealGroupStats returns the number of hackers assigned to each meal group +func (s *SettingsStore) GetMealGroupStats(ctx context.Context) (map[string]int, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := ` + SELECT meal_group, COUNT(*) + FROM applications + WHERE meal_group IS NOT NULL + GROUP BY meal_group + ` + + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + stats := make(map[string]int) + for rows.Next() { + var group string + var count int + if err := rows.Scan(&group, &count); err != nil { + return nil, err + } + stats[group] = count + } + + return stats, nil +} diff --git a/internal/store/storage.go b/internal/store/storage.go index e01a8965..301f46dc 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -37,6 +37,8 @@ type Storage struct { GetStats(ctx context.Context) (*ApplicationStats, error) SetStatus(ctx context.Context, id string, status ApplicationStatus) (*Application, error) GetEmailsByStatus(ctx context.Context, status ApplicationStatus) ([]UserEmailInfo, error) + SetMealGroup(ctx context.Context, id string, mealGroup string) error + GetMealGroupByUserID(ctx context.Context, userID string) (*string, error) } Settings interface { GetShortAnswerQuestions(ctx context.Context) ([]ShortAnswerQuestion, error) @@ -53,6 +55,9 @@ type Storage struct { GetScanTypes(ctx context.Context) ([]ScanType, error) UpdateScanTypes(ctx context.Context, scanTypes []ScanType) error GetScanStats(ctx context.Context) (map[string]int, error) + GetMealGroups(ctx context.Context) ([]string, error) + SetMealGroups(ctx context.Context, groups []string) error + GetMealGroupStats(ctx context.Context) (map[string]int, error) } Scans interface { Create(ctx context.Context, scan *Scan) error From 39498189679e6e6ec9f81c31d8aebc3b23bafb5b Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:36:16 -0500 Subject: [PATCH 3/7] chore(go test) --- cmd/api/scans_test.go | 5 +++ internal/store/applications.go | 74 ++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/cmd/api/scans_test.go b/cmd/api/scans_test.go index 38513d50..3450b9eb 100644 --- a/cmd/api/scans_test.go +++ b/cmd/api/scans_test.go @@ -128,10 +128,14 @@ func TestCreateScan(t *testing.T) { app := newTestApplication(t) mockSettings := app.store.Settings.(*store.MockSettingsStore) mockScans := app.store.Scans.(*store.MockScansStore) + mockApps := app.store.Application.(*store.MockApplicationStore) + + mealGroup := "A" mockSettings.On("GetScanTypes").Return(scanTypes, nil).Once() mockScans.On("HasCheckIn", "user-1", []string{"check_in"}).Return(true, nil).Once() mockScans.On("Create", mock.AnythingOfType("*store.Scan")).Return(nil).Once() + mockApps.On("GetMealGroupByUserID", "user-1").Return(&mealGroup, nil).Once() body := `{"user_id":"user-1","scan_type":"lunch"}` req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(body)) @@ -144,6 +148,7 @@ func TestCreateScan(t *testing.T) { mockSettings.AssertExpectations(t) mockScans.AssertExpectations(t) + mockApps.AssertExpectations(t) }) t.Run("403 not checked in", func(t *testing.T) { diff --git a/internal/store/applications.go b/internal/store/applications.go index f310af7c..9fea0532 100644 --- a/internal/store/applications.go +++ b/internal/store/applications.go @@ -143,6 +143,7 @@ type ApplicationListItem struct { ReviewsCompleted int `json:"reviews_completed"` AIPercent *int `json:"ai_percent"` HasResume bool `json:"has_resume"` + MealGroup *string `json:"meal_group"` } // ApplicationListResult contains paginated results @@ -247,7 +248,8 @@ type Application struct { ReviewsAssigned int `json:"reviews_assigned"` ReviewsCompleted int `json:"reviews_completed"` - AIPercent *int16 `json:"ai_percent"` + AIPercent *int16 `json:"ai_percent"` + MealGroup *string `json:"meal_group"` } type ApplicationsStore struct { @@ -268,8 +270,9 @@ func (s *ApplicationsStore) GetByID(ctx context.Context, id string) (*Applicatio shirt_size, dietary_restrictions, accommodations, github, linkedin, website, resume_path, ack_application, ack_mlh_coc, ack_mlh_privacy, opt_in_mlh_emails, - submitted_at, created_at, updated_at, - accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed, ai_percent + submitted_at, created_at, updated_at, meal_group, + accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed, + ai_percent FROM applications WHERE id = $1 ` @@ -285,8 +288,9 @@ func (s *ApplicationsStore) GetByID(ctx context.Context, id string) (*Applicatio &app.ShirtSize, (*StringArray)(&app.DietaryRestrictions), &app.Accommodations, &app.Github, &app.LinkedIn, &app.Website, &app.ResumePath, &app.AckApplication, &app.AckMLHCOC, &app.AckMLHPrivacy, &app.OptInMLHEmails, - &app.SubmittedAt, &app.CreatedAt, &app.UpdatedAt, - &app.AcceptVotes, &app.RejectVotes, &app.WaitlistVotes, &app.ReviewsAssigned, &app.ReviewsCompleted, &app.AIPercent, + &app.SubmittedAt, &app.CreatedAt, &app.UpdatedAt, &app.MealGroup, + &app.AcceptVotes, &app.RejectVotes, &app.WaitlistVotes, &app.ReviewsAssigned, &app.ReviewsCompleted, + &app.AIPercent, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -312,8 +316,9 @@ func (s *ApplicationsStore) GetByUserID(ctx context.Context, userID string) (*Ap shirt_size, dietary_restrictions, accommodations, github, linkedin, website, resume_path, ack_application, ack_mlh_coc, ack_mlh_privacy, opt_in_mlh_emails, - submitted_at, created_at, updated_at, - accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed + submitted_at, created_at, updated_at, meal_group, + accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed, + ai_percent FROM applications WHERE user_id = $1 ` @@ -329,8 +334,9 @@ func (s *ApplicationsStore) GetByUserID(ctx context.Context, userID string) (*Ap &app.ShirtSize, (*StringArray)(&app.DietaryRestrictions), &app.Accommodations, &app.Github, &app.LinkedIn, &app.Website, &app.ResumePath, &app.AckApplication, &app.AckMLHCOC, &app.AckMLHPrivacy, &app.OptInMLHEmails, - &app.SubmittedAt, &app.CreatedAt, &app.UpdatedAt, + &app.SubmittedAt, &app.CreatedAt, &app.UpdatedAt, &app.MealGroup, &app.AcceptVotes, &app.RejectVotes, &app.WaitlistVotes, &app.ReviewsAssigned, &app.ReviewsCompleted, + &app.AIPercent, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -400,7 +406,8 @@ func (s *ApplicationsStore) Update(ctx context.Context, app *Application) error ack_application = $23, ack_mlh_coc = $24, ack_mlh_privacy = $25, - opt_in_mlh_emails = $26 + opt_in_mlh_emails = $26, + meal_group = $28 WHERE id = $1 RETURNING updated_at ` @@ -416,6 +423,7 @@ func (s *ApplicationsStore) Update(ctx context.Context, app *Application) error app.Github, app.LinkedIn, app.Website, app.AckApplication, app.AckMLHCOC, app.AckMLHPrivacy, app.OptInMLHEmails, app.ResumePath, + app.MealGroup, ).Scan(&app.UpdatedAt) if err != nil { @@ -528,7 +536,7 @@ func (s *ApplicationsStore) List( a.hackathons_attended_count, a.submitted_at, a.created_at, a.updated_at, a.accept_votes, a.reject_votes, a.waitlist_votes, a.reviews_assigned, a.reviews_completed, a.ai_percent, - a.resume_path IS NOT NULL AS has_resume + a.resume_path IS NOT NULL AS has_resume, a.meal_group FROM applications a INNER JOIN users u ON a.user_id = u.id` @@ -623,7 +631,7 @@ func (s *ApplicationsStore) List( &item.HackathonsAttendedCount, &item.SubmittedAt, &item.CreatedAt, &item.UpdatedAt, &item.AcceptVotes, &item.RejectVotes, &item.WaitlistVotes, &item.ReviewsAssigned, &item.ReviewsCompleted, &item.AIPercent, - &item.HasResume, + &item.HasResume, &item.MealGroup, ); err != nil { return nil, err } @@ -706,7 +714,7 @@ func (s *ApplicationsStore) SetStatus(ctx context.Context, id string, status App github, linkedin, website, resume_path, ack_application, ack_mlh_coc, ack_mlh_privacy, opt_in_mlh_emails, submitted_at, created_at, updated_at, - accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed + accept_votes, reject_votes, waitlist_votes, reviews_assigned, reviews_completed, meal_group ` var app Application @@ -721,7 +729,7 @@ func (s *ApplicationsStore) SetStatus(ctx context.Context, id string, status App &app.Github, &app.LinkedIn, &app.Website, &app.ResumePath, &app.AckApplication, &app.AckMLHCOC, &app.AckMLHPrivacy, &app.OptInMLHEmails, &app.SubmittedAt, &app.CreatedAt, &app.UpdatedAt, - &app.AcceptVotes, &app.RejectVotes, &app.WaitlistVotes, &app.ReviewsAssigned, &app.ReviewsCompleted, + &app.AcceptVotes, &app.RejectVotes, &app.WaitlistVotes, &app.ReviewsAssigned, &app.ReviewsCompleted, &app.MealGroup, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -806,3 +814,43 @@ func (s *ApplicationsStore) GetEmailsByStatus(ctx context.Context, status Applic return users, rows.Err() } + +// SetMealGroup updates the meal group for a specific application +func (s *ApplicationsStore) SetMealGroup(ctx context.Context, id string, mealGroup string) error { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := ` + UPDATE applications + SET meal_group = $2, updated_at = NOW() + WHERE id = $1 + ` + + result, err := s.db.ExecContext(ctx, query, id, mealGroup) + if err != nil { + return err + } + + rows, err := result.RowsAffected() + if err != nil { + return err + } + if rows == 0 { + return ErrNotFound + } + + return nil +} + +// GetMealGroupByUserID returns the assigned meal group for a user +func (s *ApplicationsStore) GetMealGroupByUserID(ctx context.Context, userID string) (*string, error) { + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + var mealGroup *string + err := s.db.QueryRowContext(ctx, "SELECT meal_group FROM applications WHERE user_id = $1", userID).Scan(&mealGroup) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return mealGroup, err +} From 3d7a167ff61fbf25d882d35737da250ca882fa13 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:43:12 -0500 Subject: [PATCH 4/7] chore(tests) BREAKING CHANGE (eslint-plugin-boundaries) --- client/web/package-lock.json | 139 +++++------------------------------ client/web/package.json | 2 +- cmd/api/scans_test.go | 2 +- 3 files changed, 21 insertions(+), 122 deletions(-) diff --git a/client/web/package-lock.json b/client/web/package-lock.json index 407533e8..98465783 100644 --- a/client/web/package-lock.json +++ b/client/web/package-lock.json @@ -69,7 +69,7 @@ "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-boundaries": "^5.4.0", + "eslint-plugin-boundaries": "^5.1.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-simple-import-sort": "^12.1.1", @@ -372,23 +372,6 @@ "node": ">=6.9.0" } }, - "node_modules/@boundaries/elements": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-1.2.0.tgz", - "integrity": "sha512-W65Gum02liMd3hmNrLmDBX1u5BmRMcunouFjLXyhxHnNY4YlK1kTxsgfflZ5XBGSnPnO0MkiUzAcoGzYrlx0RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-import-resolver-node": "0.3.9", - "eslint-module-utils": "2.12.1", - "handlebars": "4.7.8", - "is-core-module": "2.16.1", - "micromatch": "4.0.8" - }, - "engines": { - "node": ">=18.18" - } - }, "node_modules/@date-fns/tz": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -3671,19 +3654,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", @@ -3859,9 +3829,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4506,13 +4476,12 @@ } }, "node_modules/eslint-plugin-boundaries": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-5.4.0.tgz", - "integrity": "sha512-6SQmEhXCqGrrxm9YiM24SC95CqrVi2MUOm5SDrfquceh/os8MIAvZYsDU69zvtCSb1S6UbNEmdioi1gCDc8+VQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-5.1.0.tgz", + "integrity": "sha512-tgmq22Z+hEb40D8SDr+QavrioSuMvpymA5AT0t0JoDc4lJ6+AQ1S5EzoRo5kfqoA5YEcTKlTTd0a4gYrWSNj0Q==", "dev": true, "license": "MIT", "dependencies": { - "@boundaries/elements": "1.2.0", "chalk": "4.1.2", "eslint-import-resolver-node": "0.3.9", "eslint-module-utils": "2.12.1", @@ -4753,9 +4722,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4834,28 +4803,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5467,16 +5414,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5509,13 +5446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -5639,9 +5569,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6138,16 +6068,6 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6314,9 +6234,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6418,20 +6338,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6659,9 +6565,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6696,13 +6602,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/client/web/package.json b/client/web/package.json index 35f50e12..5e3231d6 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -73,7 +73,7 @@ "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-boundaries": "^5.4.0", + "eslint-plugin-boundaries": "^5.1.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-simple-import-sort": "^12.1.1", diff --git a/cmd/api/scans_test.go b/cmd/api/scans_test.go index 3450b9eb..78496bc5 100644 --- a/cmd/api/scans_test.go +++ b/cmd/api/scans_test.go @@ -1,8 +1,8 @@ package main import ( - "errors" "encoding/json" + "errors" "net/http" "strings" "testing" From 60c6c51a213ce534b08598eedb6f15531eb8141d Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:25:17 -0500 Subject: [PATCH 5/7] chore(package conflicts) --- client/web/package-lock.json | 235 +++++++++++++++++++++++---------- client/web/package.json | 8 +- internal/store/applications.go | 6 +- 3 files changed, 176 insertions(+), 73 deletions(-) diff --git a/client/web/package-lock.json b/client/web/package-lock.json index 070b5cc7..cdf84953 100644 --- a/client/web/package-lock.json +++ b/client/web/package-lock.json @@ -79,7 +79,7 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", - "vite-plugin-pwa": "^1.2.0" + "vite-plugin-pwa": "^0.19.8" } }, "node_modules/@babel/code-frame": { @@ -113,7 +113,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2402,6 +2401,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4853,7 +4890,6 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4864,7 +4900,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4875,7 +4910,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4939,7 +4973,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5165,7 +5198,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5438,7 +5470,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6032,8 +6063,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6276,7 +6306,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6581,6 +6610,36 @@ "node": ">=6.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -6612,6 +6671,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8147,6 +8216,16 @@ "node": ">= 0.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8229,6 +8308,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -8570,22 +8656,32 @@ "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==", "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8616,7 +8712,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8629,7 +8724,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -8993,6 +9087,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -9037,6 +9142,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -9057,27 +9186,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -9133,13 +9241,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-cookie-parser": { @@ -9539,7 +9647,6 @@ "resolved": "https://registry.npmjs.org/supertokens-web-js/-/supertokens-web-js-0.16.0.tgz", "integrity": "sha512-wuIdlVJtOsx4ZX0kCyl8lxmmAodXLlMAniZEHyVhsH2fhersh7bMrHpvgN9WoC470HYNC22qpHdlJngvyh/cSA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@simplewebauthn/browser": "^13.0.0", "supertokens-js-override": "0.0.4", @@ -9646,7 +9753,6 @@ "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "devOptional": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -9704,7 +9810,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9874,7 +9979,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10158,7 +10262,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10229,17 +10332,17 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", - "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.19.8.tgz", + "integrity": "sha512-e1oK0dfhzhDhY3VBuML6c0h8Xfx6EkOVYqolj7g+u8eRfdauZe5RLteCIA/c5gH0CBQ0CNFAuv/AFTx4Z7IXTw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.6", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", "pretty-bytes": "^6.1.1", - "tinyglobby": "^0.2.10", - "workbox-build": "^7.4.0", - "workbox-window": "^7.4.0" + "workbox-build": "^7.0.0", + "workbox-window": "^7.0.0" }, "engines": { "node": ">=16.0.0" @@ -10248,10 +10351,10 @@ "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vite-pwa/assets-generator": "^1.0.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "workbox-build": "^7.4.0", - "workbox-window": "^7.4.0" + "@vite-pwa/assets-generator": "^0.2.4", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0", + "workbox-build": "^7.0.0", + "workbox-window": "^7.0.0" }, "peerDependenciesMeta": { "@vite-pwa/assets-generator": { @@ -10281,7 +10384,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10586,7 +10688,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10641,7 +10742,6 @@ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -10828,7 +10928,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/web/package.json b/client/web/package.json index 288fc56d..88f8df63 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -83,11 +83,15 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", - "vite-plugin-pwa": "^1.2.0" + "vite-plugin-pwa": "^0.19.8" }, "overrides": { "postcss": "^8.4.38", "semver": "^7.5.2", - "minimatch": "^10.2.1" + "minimatch": "^10.2.1", + "serialize-javascript": "^7.0.5", + "vite-plugin-pwa": { + "vite": "$vite" + } } } diff --git a/internal/store/applications.go b/internal/store/applications.go index 3ae4ed94..bfc2db48 100644 --- a/internal/store/applications.go +++ b/internal/store/applications.go @@ -79,7 +79,7 @@ type ApplicationListItem struct { ReviewsCompleted int `json:"reviews_completed"` AIPercent *int `json:"ai_percent"` HasResume bool `json:"has_resume"` - MealGroup *string `json:"meal_group"` + MealGroup *string `json:"meal_group"` } // ApplicationListResult contains paginated results @@ -151,9 +151,9 @@ type Application struct { ReviewsCompleted int `json:"reviews_completed"` SubmittedAt *time.Time `json:"submitted_at"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - MealGroup *string `json:"meal_group"` + MealGroup *string `json:"meal_group"` } type ApplicationsStore struct { From 5c179b32695f362199cf92b747415b0aec96c043 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:30:59 -0500 Subject: [PATCH 6/7] chore(review fixes) --- cmd/api/applications.go | 8 +++++++ cmd/api/auth.go | 3 +++ cmd/api/reset_hackathon.go | 1 + cmd/api/resume.go | 3 +++ cmd/api/reviews.go | 7 ++++++ cmd/api/scans.go | 43 ++++++++++++++++++++++++------------- cmd/api/schedule.go | 4 ++++ cmd/api/settings.go | 15 ++++++++++++- cmd/api/sponsors.go | 4 ++++ cmd/api/superadmin_users.go | 3 +++ 10 files changed, 75 insertions(+), 16 deletions(-) diff --git a/cmd/api/applications.go b/cmd/api/applications.go index 56f82824..c50216b9 100644 --- a/cmd/api/applications.go +++ b/cmd/api/applications.go @@ -80,6 +80,7 @@ func (app *application) getOrCreateApplicationHandler(w http.ResponseWriter, r * if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -141,6 +142,7 @@ func (app *application) updateApplicationHandler(w http.ResponseWriter, r *http. if err := app.jsonResponse(w, http.StatusOK, application); err != nil { app.internalServerError(w, r, err) + return } } @@ -212,6 +214,7 @@ func (app *application) submitApplicationHandler(w http.ResponseWriter, r *http. if err := app.jsonResponse(w, http.StatusOK, application); err != nil { app.internalServerError(w, r, err) + return } } @@ -351,6 +354,7 @@ func (app *application) getApplicationStatsHandler(w http.ResponseWriter, r *htt if err := app.jsonResponse(w, http.StatusOK, stats); err != nil { app.internalServerError(w, r, err) + return } } @@ -456,6 +460,7 @@ func (app *application) listApplicationsHandler(w http.ResponseWriter, r *http.R if err := app.jsonResponse(w, http.StatusOK, result); err != nil { app.internalServerError(w, r, err) + return } } @@ -525,6 +530,7 @@ func (app *application) setApplicationStatus(w http.ResponseWriter, r *http.Requ if err := app.jsonResponse(w, http.StatusOK, ApplicationResponse{Application: application}); err != nil { app.internalServerError(w, r, err) + return } } @@ -573,6 +579,7 @@ func (app *application) getApplication(w http.ResponseWriter, r *http.Request) { if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -627,5 +634,6 @@ func (app *application) getApplicantEmailsByStatusHandler(w http.ResponseWriter, if err = app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/auth.go b/cmd/api/auth.go index 0013bbd8..e7d71504 100644 --- a/cmd/api/auth.go +++ b/cmd/api/auth.go @@ -46,6 +46,7 @@ func (app *application) getCurrentUserHandler(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -79,6 +80,7 @@ func (app *application) checkEmailAuthMethodHandler(w http.ResponseWriter, r *ht // Email not registered if err := app.jsonResponse(w, http.StatusOK, CheckEmailResponse{Exists: false}); err != nil { app.internalServerError(w, r, err) + return } return } @@ -92,5 +94,6 @@ func (app *application) checkEmailAuthMethodHandler(w http.ResponseWriter, r *ht AuthMethod: &user.AuthMethod, }); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/reset_hackathon.go b/cmd/api/reset_hackathon.go index fc8902d6..b0c30e4a 100644 --- a/cmd/api/reset_hackathon.go +++ b/cmd/api/reset_hackathon.go @@ -78,5 +78,6 @@ func (app *application) resetHackathonHandler(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/resume.go b/cmd/api/resume.go index 445e9fb5..1574a647 100644 --- a/cmd/api/resume.go +++ b/cmd/api/resume.go @@ -83,6 +83,7 @@ func (app *application) generateResumeUploadURLHandler(w http.ResponseWriter, r ResumePath: objectPath, }); err != nil { app.internalServerError(w, r, err) + return } } @@ -142,6 +143,7 @@ func (app *application) deleteResumeHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, application); err != nil { app.internalServerError(w, r, err) + return } } @@ -199,6 +201,7 @@ func (app *application) getResumeDownloadURLHandler(w http.ResponseWriter, r *ht DownloadURL: downloadURL, }); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/reviews.go b/cmd/api/reviews.go index 3859963c..e8e5c06c 100644 --- a/cmd/api/reviews.go +++ b/cmd/api/reviews.go @@ -64,6 +64,7 @@ func (app *application) getPendingReviews(w http.ResponseWriter, r *http.Request if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -94,6 +95,7 @@ func (app *application) getCompletedReviews(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -130,6 +132,7 @@ func (app *application) getApplicationNotes(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -160,6 +163,7 @@ func (app *application) batchAssignReviews(w http.ResponseWriter, r *http.Reques if err := app.jsonResponse(w, http.StatusOK, result); err != nil { app.internalServerError(w, r, err) + return } } @@ -202,6 +206,7 @@ func (app *application) getNextReview(w http.ResponseWriter, r *http.Request) { if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -259,6 +264,7 @@ func (app *application) submitVote(w http.ResponseWriter, r *http.Request) { if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -317,5 +323,6 @@ func (app *application) setAIPercent(w http.ResponseWriter, r *http.Request) { if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/scans.go b/cmd/api/scans.go index 54dcae87..3dac39ae 100644 --- a/cmd/api/scans.go +++ b/cmd/api/scans.go @@ -33,7 +33,7 @@ type UpdateScanTypesPayload struct { type CreateScanResponse struct { *store.Scan - MealGroup *string `json:"meal_group"` + MealGroup *string `json:"meal_group,omitempty"` } // getScanTypesHandler returns all configured scan types @@ -57,6 +57,7 @@ func (app *application) getScanTypesHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, ScanTypesResponse{ScanTypes: scanTypes}); err != nil { app.internalServerError(w, r, err) + return } } @@ -155,15 +156,17 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request return } + var mealGroup *string if found.Category == store.ScanCategoryCheckIn { - app.assignMealGroup(r.Context(), req.UserID) - } - - // Fetch meal group for response (non-fatal) - mealGroup, err := app.store.Application.GetMealGroupByUserID(r.Context(), req.UserID) - if err != nil && !errors.Is(err, store.ErrNotFound) { - // We don't want to fail the scan if we can't get the meal group info - app.logger.Warnw("failed to fetch meal group for scan response", "user_id", req.UserID, "error", err) + mealGroup = app.assignMealGroup(r.Context(), req.UserID) + } else { + // Fetch meal group for response (non-fatal) + var err error + mealGroup, err = app.store.Application.GetMealGroupByUserID(r.Context(), req.UserID) + if err != nil && !errors.Is(err, store.ErrNotFound) { + // We don't want to fail the scan if we can't get the meal group info + app.logger.Warnw("failed to fetch meal group for scan response", "user_id", req.UserID, "error", err) + } } response := CreateScanResponse{ @@ -173,6 +176,7 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request if err := app.jsonResponse(w, http.StatusCreated, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -205,34 +209,41 @@ func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, ScansResponse{Scans: scans}); err != nil { app.internalServerError(w, r, err) + return } } -func (app *application) assignMealGroup(ctx context.Context, userID string) { +func (app *application) assignMealGroup(ctx context.Context, userID string) *string { groups, err := app.store.Settings.GetMealGroups(ctx) if err != nil { app.logger.Warnw("failed to fetch meal groups for assignment", "error", err) - return + return nil } if len(groups) == 0 { - return + return nil } hackerApp, err := app.store.Application.GetByUserID(ctx, userID) if err != nil { - app.logger.Warnw("failed to fetch application for meal group assignment", "user_id", userID, "error", err) - return + // If the user doesn't have an application, we can't assign a group. + if !errors.Is(err, store.ErrNotFound) { + app.logger.Warnw("failed to fetch application for meal group assignment", "user_id", userID, "error", err) + } + return nil } if hackerApp.MealGroup != nil { - return // Already assigned + return hackerApp.MealGroup // Already assigned } selectedGroup := groups[rand.Intn(len(groups))] if err := app.store.Application.SetMealGroup(ctx, hackerApp.ID, selectedGroup); err != nil { app.logger.Warnw("failed to set meal group on application", "app_id", hackerApp.ID, "error", err) + return nil } + + return &selectedGroup } // getScanStatsHandler returns aggregate scan counts grouped by scan type @@ -256,6 +267,7 @@ func (app *application) getScanStatsHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, ScanStatsResponse{Stats: stats}); err != nil { app.internalServerError(w, r, err) + return } } @@ -315,5 +327,6 @@ func (app *application) updateScanTypesHandler(w http.ResponseWriter, r *http.Re if err := app.jsonResponse(w, http.StatusOK, ScanTypesResponse(req)); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/schedule.go b/cmd/api/schedule.go index 4533ad0c..d3a1255d 100644 --- a/cmd/api/schedule.go +++ b/cmd/api/schedule.go @@ -58,6 +58,7 @@ func (app *application) getAdminScheduleDateRange(w http.ResponseWriter, r *http if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -82,6 +83,7 @@ func (app *application) listScheduleHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, ScheduleListResponse{Schedule: items}); err != nil { app.internalServerError(w, r, err) + return } } @@ -138,6 +140,7 @@ func (app *application) createScheduleHandler(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusCreated, ScheduleItemResponse{Schedule: *item}); err != nil { app.internalServerError(w, r, err) + return } } @@ -203,6 +206,7 @@ func (app *application) updateScheduleHandler(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, ScheduleItemResponse{Schedule: *item}); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/settings.go b/cmd/api/settings.go index 6fabe592..963e9402 100644 --- a/cmd/api/settings.go +++ b/cmd/api/settings.go @@ -41,6 +41,7 @@ func (app *application) getApplicationSchema(w http.ResponseWriter, r *http.Requ if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -90,6 +91,7 @@ func (app *application) updateApplicationSchema(w http.ResponseWriter, r *http.R if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -126,6 +128,7 @@ func (app *application) getReviewsPerApp(w http.ResponseWriter, r *http.Request) if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -165,6 +168,7 @@ func (app *application) setReviewsPerApp(w http.ResponseWriter, r *http.Request) if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -258,6 +262,7 @@ func (app *application) setReviewAssignmentToggle(w http.ResponseWriter, r *http if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -286,6 +291,7 @@ func (app *application) getAdminScheduleEditToggle(w http.ResponseWriter, r *htt if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -320,6 +326,7 @@ func (app *application) setAdminScheduleEditToggle(w http.ResponseWriter, r *htt if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -350,6 +357,7 @@ func (app *application) getHackathonDateRange(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } @@ -420,11 +428,12 @@ func (app *application) setHackathonDateRange(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } type UpdateMealGroupsPayload struct { - Groups []string `json:"groups" validate:"required,min=1,dive,required,min=1,max=50"` + Groups []string `json:"groups" validate:"max=50,dive,required,min=1,max=50"` } type MealGroupsResponse struct { @@ -456,6 +465,7 @@ func (app *application) getMealGroups(w http.ResponseWriter, r *http.Request) { if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse{Groups: groups}); err != nil { app.internalServerError(w, r, err) + return } } @@ -503,6 +513,7 @@ func (app *application) updateMealGroups(w http.ResponseWriter, r *http.Request) if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse(req)); err != nil { app.internalServerError(w, r, err) + return } } @@ -527,6 +538,7 @@ func (app *application) getMealGroupStats(w http.ResponseWriter, r *http.Request if err := app.jsonResponse(w, http.StatusOK, MealGroupStatsResponse{Stats: stats}); err != nil { app.internalServerError(w, r, err) + return } } @@ -589,5 +601,6 @@ func (app *application) setApplicationsEnabled(w http.ResponseWriter, r *http.Re if err := app.jsonResponse(w, http.StatusOK, response); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/sponsors.go b/cmd/api/sponsors.go index 72130d05..e585e200 100644 --- a/cmd/api/sponsors.go +++ b/cmd/api/sponsors.go @@ -57,6 +57,7 @@ func (app *application) listSponsorsHandler(w http.ResponseWriter, r *http.Reque if err := app.jsonResponse(w, http.StatusOK, SponsorListResponse{Sponsors: sponsors}); err != nil { app.internalServerError(w, r, err) + return } } @@ -102,6 +103,7 @@ func (app *application) createSponsorHandler(w http.ResponseWriter, r *http.Requ if err := app.jsonResponse(w, http.StatusCreated, sponsor); err != nil { app.internalServerError(w, r, err) + return } } @@ -160,6 +162,7 @@ func (app *application) updateSponsorHandler(w http.ResponseWriter, r *http.Requ if err := app.jsonResponse(w, http.StatusOK, sponsor); err != nil { app.internalServerError(w, r, err) + return } } @@ -272,5 +275,6 @@ func (app *application) uploadLogoHandler(w http.ResponseWriter, r *http.Request if err := app.jsonResponse(w, http.StatusOK, sponsor); err != nil { app.internalServerError(w, r, err) + return } } diff --git a/cmd/api/superadmin_users.go b/cmd/api/superadmin_users.go index a2f2450d..4f7abb40 100644 --- a/cmd/api/superadmin_users.go +++ b/cmd/api/superadmin_users.go @@ -103,6 +103,7 @@ func (app *application) searchUsersHandler(w http.ResponseWriter, r *http.Reques if err := app.jsonResponse(w, http.StatusOK, result); err != nil { app.internalServerError(w, r, err) + return } } @@ -200,6 +201,7 @@ func (app *application) listUsersByRole(w http.ResponseWriter, r *http.Request, } if err := app.jsonResponse(w, http.StatusOK, resp); err != nil { app.internalServerError(w, r, err) + return } } @@ -250,5 +252,6 @@ func (app *application) updateUserRoleHandler(w http.ResponseWriter, r *http.Req if err := app.jsonResponse(w, http.StatusOK, UpdateRoleResponse{User: user}); err != nil { app.internalServerError(w, r, err) + return } } From dc3876aa7f59d9b1ef53a49e55ff0ff70ff944c1 Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:36:19 -0500 Subject: [PATCH 7/7] chore() --- cmd/api/scans_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/api/scans_test.go b/cmd/api/scans_test.go index 78496bc5..263727ce 100644 --- a/cmd/api/scans_test.go +++ b/cmd/api/scans_test.go @@ -66,7 +66,6 @@ func TestCreateScan(t *testing.T) { mockSettings.On("GetMealGroups").Return(groups, nil).Once() mockApps.On("GetByUserID", "user-1").Return(hackerApp, nil).Once() mockApps.On("SetMealGroup", "app-1", mock.AnythingOfType("string")).Return(nil).Once() - mockApps.On("GetMealGroupByUserID", "user-1").Return(&groups[0], nil).Once() mockScans.On("Create", mock.AnythingOfType("*store.Scan")).Return(nil).Once() body := `{"user_id":"user-1","scan_type":"check_in"}` @@ -84,7 +83,7 @@ func TestCreateScan(t *testing.T) { err = json.NewDecoder(rr.Body).Decode(&resp) require.NoError(t, err) assert.NotNil(t, resp.Data.MealGroup) - assert.Equal(t, groups[0], *resp.Data.MealGroup) + assert.Contains(t, groups, *resp.Data.MealGroup) mockSettings.AssertExpectations(t) mockScans.AssertExpectations(t) @@ -100,7 +99,6 @@ func TestCreateScan(t *testing.T) { mockSettings.On("GetScanTypes").Return(scanTypes, nil).Once() // Simulate error in meal group fetching mockSettings.On("GetMealGroups").Return(nil, errors.New("db error")).Once() - mockApps.On("GetMealGroupByUserID", "user-1").Return(nil, store.ErrNotFound).Once() mockScans.On("Create", mock.AnythingOfType("*store.Scan")).Return(nil).Once() body := `{"user_id":"user-1","scan_type":"check_in"}`