You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Last sprint shipped the three date singletons. What's missing is the show-decision singleton (needs the POST handler), the form-config service and endpoints, and admin gating on all four singleton writes.
Tasks
Complete the show-decision singleton
The GET handler exists from last sprint (or from sprint 1 scaffold — verify). Implement the POST handler in src/app/api/v1/show-decision/route.ts following the same pattern as the date singletons: parse { value } from the body, validate it's a boolean, call setSingleton('show-decision', value, updatedBy), return the response.
What this accomplishes: The admin settings UI's show-decision toggle now actually persists. Right now the toggle probably calls the endpoint and gets a 501 or does nothing.
Add a validateBooleanSingleton helper to src/lib/admin/singleton-service.ts for the value check.
What this accomplishes: Mirrors the pattern of validateDateSingleton for consistency. Prevents non-boolean values from being written.
Form config service
Design the form config document shape. Recommended: single document in singleton_data with key form-config, value shape { sections: [{ id, title, questions: [{ id, type, label, required, options?, order }] }] }.
What this accomplishes: One source of truth for what questions the application asks. Sections group related questions on the form.
Implement getFormConfig() in src/lib/admin/form-config-service.ts. Reads via getSingleton('form-config'), returns the config or a sensible default (the current frozen questions.ts structure).
What this accomplishes: Any part of the app can fetch the current form config. Falling back to questions.ts means a fresh install still works before an admin has customized anything.
Implement updateFormConfig(config, updatedBy). Validates config shape, enforces two rules — unique question IDs and no dropping IDs that already have responses — and writes via setSingleton.
What this accomplishes: Prevents admins from accidentally corrupting the applicant data by renaming, duplicating, or deleting questions that people have already answered.
For the "no dropping IDs in use" rule, before writing, query applicant_data and collect all question IDs referenced in existing applicationResponses objects. If the new config drops any of them, reject with 400.
What this accomplishes: Editing questions mid-application-cycle is a real admin need (typos, added clarifications), but it must not silently orphan applicant data.
Form config endpoints
Implement GET /api/v1/admin/form-config/route.ts. Calls getFormConfig(), returns the config.
What this accomplishes: The admin settings UI's form config editor loads the current config from here.
Implement POST on the same route. Parses the body, calls updateFormConfig, returns success or the validation error.
What this accomplishes: The "save form config" button writes the new config.
Consumer integration
The application form's frontend currently reads from a hardcoded questions.ts. This sprint's scope is just to make getFormConfig() and the endpoint work; wiring the applicant form to fetch from /api/v1/admin/form-config (or from the server) is Ticket 2's frontend concern and can slide to next sprint.
What this accomplishes: Keeps this ticket focused on the admin side. The applicant-side integration is a small follow-up.
Auth gating
Wire requireAdmin() into every write endpoint: POST on all three date singletons, POST on show-decision, POST on form-config.
What this accomplishes: Only admins can change configuration. Any authenticated user can still GET these values (needed by the applicant-side status machine).
Update the updatedBy field on writes to come from session.user.email instead of the placeholder.
What this accomplishes: Real audit trail. Every config change is traceable to a specific admin.
Definition of done
The show-decision toggle on /admin/settings persists on save.
Admins can add, edit, reorder, and delete questions via /admin/settings, and the changes persist.
Attempting to save a form config with duplicate question IDs or with a dropped-but-in-use ID returns a 400.
Every write endpoint under settings is requireAdmin()-gated.
Context
Last sprint shipped the three date singletons. What's missing is the show-decision singleton (needs the POST handler), the form-config service and endpoints, and admin gating on all four singleton writes.
Tasks
Complete the show-decision singleton
src/app/api/v1/show-decision/route.tsfollowing the same pattern as the date singletons: parse{ value }from the body, validate it's a boolean, callsetSingleton('show-decision', value, updatedBy), return the response.validateBooleanSingletonhelper tosrc/lib/admin/singleton-service.tsfor the value check.validateDateSingletonfor consistency. Prevents non-boolean values from being written.Form config service
singleton_datawith keyform-config, value shape{ sections: [{ id, title, questions: [{ id, type, label, required, options?, order }] }] }.getFormConfig()insrc/lib/admin/form-config-service.ts. Reads viagetSingleton('form-config'), returns the config or a sensible default (the current frozenquestions.tsstructure).questions.tsmeans a fresh install still works before an admin has customized anything.updateFormConfig(config, updatedBy). Validates config shape, enforces two rules — unique question IDs and no dropping IDs that already have responses — and writes viasetSingleton.applicant_dataand collect all question IDs referenced in existingapplicationResponsesobjects. If the new config drops any of them, reject with 400.Form config endpoints
GET /api/v1/admin/form-config/route.ts. CallsgetFormConfig(), returns the config.POSTon the same route. Parses the body, callsupdateFormConfig, returns success or the validation error.Consumer integration
questions.ts. This sprint's scope is just to makegetFormConfig()and the endpoint work; wiring the applicant form to fetch from/api/v1/admin/form-config(or from the server) is Ticket 2's frontend concern and can slide to next sprint.Auth gating
requireAdmin()into every write endpoint:POSTon all three date singletons,POSTon show-decision,POSTon form-config.GETthese values (needed by the applicant-side status machine).updatedByfield on writes to come fromsession.user.emailinstead of the placeholder.Definition of done
/admin/settingspersists on save./admin/settings, and the changes persist.requireAdmin()-gated.