docs/plans/2_19/survey-vars-reusable.md
Survey Variables are currently defined inline on each Task Template (stored as a
JSON blob in project__template.survey_vars). The issue asks that Survey Variables
become a separate, reusable entity that can be created once and assigned to many
Task Templates — exactly like an Environment ("Variable Group") is today.
"Survey Variables should be set separate to Task Templates and then assigned to a task template like Environment is. Then when you want to change survey variables, you don't have to modify all your task templates one by one."
We introduce a new project-scoped entity — a Survey — which is a named collection
of SurveyVar definitions. Templates reference Surveys through a M:N junction
table, mirroring how project__template_environment links templates to
environments.
Key decisions:
TemplateForm.vue). Survey mirrors this 1:1 to minimize design risk.EnvironmentIDs.Template.SurveyVars field is kept and still works. At task-run time the
effective survey variable list is the merge of inline vars + all assigned
Surveys' vars. No data migration is forced on users.db.Survey), table = project__survey, junction =
project__template_survey. UI label: "Surveys".Current relevant code (from research):
db/Template.go — SurveyVar, SurveyVarEnumValue, SurveyVarType, and
Template struct (SurveyVarsJSON, SurveyVars).db/Environment.go — entity to mirror.db/Store.go — EnvironmentManager interface (lines ~321-330),
EnvironmentProps, TemplateManager (GetTemplateEnvironments,
UpdateTemplateEnvironments).db/sql/environment.go, db/sql/template.go — SQL CRUD to mirror.db/sql/migrations/ — latest is v2.9.97.sql; new migration goes after it.api/router.go — environment subrouter (lines ~306-307, ~426-433) to mirror.api/projects/ — environment + template controllers.web/src/views/project/Environment.vue, web/src/components/EnvironmentForm.vue,
web/src/components/TemplateForm.vue, web/src/components/SurveyVars.vue.template.SurveyVars (validation of survey answers
when a task is created/run).1.1 New db/Survey.go
type Survey struct {
ID int `db:"id" json:"id" backup:"-"`
ProjectID int `db:"project_id" json:"project_id" backup:"-"`
Name string `db:"name" json:"name" binding:"required"`
// VarsJSON is the persisted form; Vars is the working form.
VarsJSON *string `db:"vars" json:"-" backup:"-"`
Vars []SurveyVar `db:"-" json:"vars" backup:"vars"`
}
SurveyVar / SurveyVarEnumValue / SurveyVarType types from
db/Template.go — do not duplicate them.FillSurvey helper that unmarshals VarsJSON → Vars (mirror
FillTemplate's survey-vars handling in db/Template.go).1.2 Template struct (db/Template.go)
Add, alongside EnvironmentIDs:
// SurveyIDs is the list of reusable Surveys assigned to this template.
// Their variables are merged with the template's inline SurveyVars at run time.
SurveyIDs []int `db:"-" bolt:"include" json:"survey_ids" backup:"-"`
Keep SurveyVarsJSON / SurveyVars unchanged (inline vars, deprecated path but
still supported).
1.3 db/Store.go
SurveyProps ObjectProps (table project__survey, sortable by name,
referring suffix survey_id).SurveyManager interface mirroring EnvironmentManager:
type SurveyManager interface {
GetSurvey(projectID, surveyID int) (Survey, error)
GetSurveyRefs(projectID, surveyID int) (ObjectReferrers, error)
GetSurveys(projectID int, params RetrieveQueryParams) ([]Survey, error)
CreateSurvey(survey Survey) (Survey, error)
UpdateSurvey(survey Survey) error
DeleteSurvey(projectID, surveyID int) error
}
TemplateManager: GetTemplateSurveys(projectID, templateID int) ([]int, error)
and UpdateTemplateSurveys(projectID, templateID int, surveyIDs []int) error.SurveyManager into the aggregate Store interface.New file db/sql/migrations/v2.9.98.sql (use the next available version; verify the
latest before assigning):
create table `project__survey` (
`id` integer primary key autoincrement,
`project_id` int not null,
`name` varchar(255) not null,
`vars` longtext,
foreign key (`project_id`) references `project`(`id`) on delete cascade
);
create table `project__template_survey` (
`project_id` int not null,
`template_id` int not null,
`survey_id` int not null,
primary key (`template_id`, `survey_id`),
foreign key (`project_id`) references `project`(`id`) on delete cascade,
foreign key (`template_id`) references `project__template`(`id`) on delete cascade,
foreign key (`survey_id`) references `project__survey`(`id`) on delete cascade
);
project__template.survey_vars stays.3.1 SQL — db/sql/survey.go (new)
Mirror db/sql/environment.go: GetSurvey, GetSurveys, CreateSurvey,
UpdateSurvey, DeleteSurvey (via generic deleteObject), GetSurveyRefs (query
project__template_survey). Serialize Vars with db.ObjectToJSON.
3.2 SQL — db/sql/template.go
GetTemplateSurveys / UpdateTemplateSurveys against
project__template_survey (copy the *TemplateEnvironments logic).CreateTemplate / UpdateTemplate / GetTemplate(s), also load & persist
SurveyIDs (same place EnvironmentIDs is handled).3.3 Cascade on delete
DeleteSurvey must remove junction rows (FK on delete cascade covers this).
GetSurveyRefs should let the UI warn / block deletion when a Survey is still
assigned.
4.1 api/router.go — add a survey subrouter mirroring environment:
GET /api/project/{project_id}/surveys
POST /api/project/{project_id}/surveys
GET /api/project/{project_id}/surveys/{survey_id}
PUT /api/project/{project_id}/surveys/{survey_id}
DELETE /api/project/{project_id}/surveys/{survey_id}
GET /api/project/{project_id}/surveys/{survey_id}/refs
4.2 api/projects/surveys.go (new) — controller + SurveyMiddleware mirroring
api/projects/environment.go. Permission: gate create/update/delete on
manageProjectResources (same as Environment).
4.3 Template controller — accept and return survey_ids; call
UpdateTemplateSurveys from AddTemplate / UpdateTemplate.
This is the behavioral core — find where template.SurveyVars is read when a task
is created/validated (validation of provided survey answers).
EffectiveSurveyVars(template, surveys []Survey) []SurveyVar
that returns template.SurveyVars ++ each assigned Survey's Vars.template.SurveyVars directly.6.1 List view web/src/views/project/Survey.vue (new) — mirror
Environment.vue: list, New/Edit/Delete, manageProjectResources gate. Add the
route + nav item next to "Environment".
6.2 web/src/components/SurveyForm.vue (new) — name field + an embedded survey
editor. Reuse SurveyVars.vue's editor logic by extracting the variable-editing
portion into a shared child component so both the inline template editor and the new
Survey entity use identical UI.
6.3 web/src/components/TemplateForm.vue — add a multi-select for Surveys next
to the Environment multi-select (v-model="item.survey_ids", :items="surveys",
multiple). Keep the existing inline SurveyVars.vue editor (still supported).
6.4 API client — add survey CRUD calls; load the project's surveys for the TemplateForm dropdown.
6.5 Task launch dialog — the survey-answer prompt must render the merged effective survey vars. Confirm whether the dialog reads vars from the template payload or fetches them; adjust so assigned Surveys' vars appear.
Survey already has backup struct tags. Update the project backup/restore code
(search backup: usage for environments) to include Surveys and the
template→survey assignments, so import/export round-trips.web/src/lang/.Per .claude/CLAUDE.md (testify, _test.go next to source):
db/sql/survey_test.go — CRUD + GetSurveyRefs + delete-cascade.api/projects/surveys_test.go — handler tests via httptest.survey_ids round-trip.survey_vars untouched.survey_ids is additive and optional everywhere (JSON omitempty-friendly).Environment
is shown as "Variable Group" in newer UI; pick a consistent user-facing label.