Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
f14e5de
Add CLAUDE.md for project guidance and setup instructions
rubenarslan Apr 23, 2026
c6b880f
plan for form_v2
rubenarslan Apr 23, 2026
e916e1c
form_v2 Phase 0: add Form RunUnit scaffold behind feature flag
rubenarslan Apr 23, 2026
39a8aaf
docs: Playwright MCP operational gotchas + gitignore its cache
rubenarslan Apr 23, 2026
b08d3c3
gitignore: Playwright MCP cache directory
rubenarslan Apr 23, 2026
08ec0e9
form_v2 Phase 1: single-page AJAX form with dedicated bundle
rubenarslan Apr 23, 2026
d803280
form_v2 Phase 1: multi-page AJAX navigation
rubenarslan Apr 23, 2026
677e717
plan_form_v2: add Progress checklist mirroring the phase definitions
rubenarslan Apr 23, 2026
406b70a
form_v2 Phase 1.5: multi-page processing, geopoint, inline BS5 errors
rubenarslan Apr 23, 2026
cb10b75
form_v2 Phase 1.5: TomSelect, BS3->BS5 compat polish, IntersectionObs…
rubenarslan Apr 23, 2026
b6252db
form_v2: match PHP $_POST semantics in JSON collectPayload
rubenarslan Apr 23, 2026
65630a5
form_v2 Phase 3: client-side showif evaluator
rubenarslan Apr 24, 2026
625f250
plan_form_v2: mark Phase 1.5 and Phase 3 progress
rubenarslan Apr 24, 2026
045a76b
docs: capture form_v2 implementation learnings
rubenarslan Apr 24, 2026
4aa21ea
Share ui-playwright-tester subagent; gitignore local .claude cruft
rubenarslan Apr 24, 2026
56d9385
form_v2 Phase 3: r(...) opt-in server-side showif + allowlist
rubenarslan Apr 24, 2026
70f542c
form_v2 Phase 2: multipart file upload path
rubenarslan Apr 24, 2026
d9fa5fe
docs: form_v2 learnings from Phase 3 r-call + Phase 2 file upload
rubenarslan Apr 24, 2026
c4432d4
form_v2 Phase 3: JS showif std-lib helpers + is.na semantics fix
rubenarslan Apr 24, 2026
a36952b
form_v2 Phase 4: deferred fill for r(...)-wrapped value columns
rubenarslan Apr 24, 2026
fdf4200
form_v2 Phase 5: page-lifetime offline queue for JSON submissions
rubenarslan Apr 24, 2026
c77e50a
form_v2 Phase 2: button groups without webshim or jQuery
rubenarslan Apr 24, 2026
b5a500e
form_v2 Phase 3: Alpine-driven showif replaces hand-rolled evaluator
rubenarslan Apr 24, 2026
5a06276
form_v2 Phase 3: compat-scan CLI for showif/value upgrade audit
rubenarslan Apr 24, 2026
570afb6
form_v2 docs: capture Alpine-session learnings
rubenarslan Apr 24, 2026
8c2e2f1
form_v2: plan rewrite + per-study admin flags + unverified-types notice
rubenarslan Apr 24, 2026
d3fb56c
form_v2 UI: two-column layout, select_or_add wiring, monkey bar, debu…
rubenarslan Apr 24, 2026
20e3154
form_v2: SW Background Sync, file-blob queue, compat UI, rate-limit, …
rubenarslan Apr 24, 2026
83c1432
form_v2 Phase 4+: r-call result cache (patch 052)
rubenarslan Apr 24, 2026
8d9d6c8
form_v2: vanilla PWA port — AddToHomeScreen + PushNotification items …
rubenarslan Apr 25, 2026
171bca4
form_v2: admin documentation
rubenarslan Apr 25, 2026
6bb8d06
form_v2: page-scoped resolver redesign + proper PWA install lib
rubenarslan Apr 25, 2026
d0514c9
form_v2: iOS Chrome install path — fall through to <pwa-install> dialog
rubenarslan Apr 25, 2026
5662b06
form_v2: push notification UX mirrors v1 (status, controls, help, test)
rubenarslan Apr 25, 2026
3feeebd
docs+deps: browser/PWA testing stack guide + chrome-devtools-mcp / pl…
rubenarslan Apr 25, 2026
a94ef45
form_v2: value column is R-only — bare R is valid, no wrapping required
rubenarslan Apr 25, 2026
ec3fac3
form_v2: drop r() unwrap shim on value column
rubenarslan Apr 25, 2026
ca5604c
e2e: Playwright smoke + BrowserStack real-device wiring (no tunnel)
rubenarslan Apr 25, 2026
c969fd8
form_v2: inline validation feedback + FOUC fix + BS projects parked
rubenarslan Apr 25, 2026
47479c7
form_v2: keep gating customValidity across submitPage cycles
rubenarslan Apr 25, 2026
7f1f1d0
form_v2: v1↔v2 parity test suite + bundle split + audio/video recorders
rubenarslan Apr 26, 2026
d2647fc
e2e: BrowserStack real-device tests unparked (npm run test:bs)
rubenarslan Apr 26, 2026
9b2a2ce
e2e: BS-only tests harden for real-device timing (10/14 green)
rubenarslan Apr 26, 2026
dd5a2d9
e2e+sw: BS real-device hardening (10/14 + 4 known platform quirks)
rubenarslan Apr 26, 2026
52d58ec
form_v2: cache showif transpile in survey_items.showif_js (patch 053)
rubenarslan Apr 26, 2026
92dddd0
form_v2: per-instance config for offline-queue blob cap
rubenarslan Apr 26, 2026
bc2d68f
e2e+plan: date/time render smoke + §8 status pass
rubenarslan Apr 26, 2026
8511aa4
plan_form_v2: drop hardened-JS-transpiler from open work
rubenarslan Apr 26, 2026
d2d5313
form_v2: defer first-page labels through /form-render-page too
rubenarslan Apr 26, 2026
b99e11e
plan_form_v2: first-page label deferral landed
rubenarslan Apr 26, 2026
cb12073
form_v2+e2e: fix /form-render-page query, BS-safe page-fixture helper
rubenarslan Apr 26, 2026
41d42f6
abridge claude md
rubenarslan Apr 27, 2026
52056e2
form_v2: prepend overlay as knitr chunk in /form-r-call
rubenarslan Apr 27, 2026
c0d8446
e2e: migrate v1/v2 suites to BS-safe page-fixture freshParticipant
rubenarslan Apr 27, 2026
0cd0af9
add: create settings.json for frontend design plugin configuration
rubenarslan Apr 27, 2026
e611ae0
remove accidentally commited screenshot
rubenarslan Apr 27, 2026
b1246d9
plan_form_v2: reconcile status doc with on-branch reality
rubenarslan Apr 27, 2026
08b880b
Update FormV2CompatScanner to enhance report structure and output
rubenarslan Apr 28, 2026
c8eacf1
Merge branch 'master' into feature/form_v2
rubenarslan Apr 29, 2026
a8272d3
form_v2: rebuild frontend bundle against jQuery 2.2.4
rubenarslan Apr 30, 2026
2a56ee2
e2e: BS iOS hardening — worker-scoped page fixture, SW polling
rubenarslan Apr 30, 2026
32fefdb
e2e: bsSafeEvaluate to bypass BS iOS Selenium arg/return mangling
rubenarslan Apr 30, 2026
823f612
e2e: single-evaluate fillAllVisible path for BS iOS Safari
rubenarslan Apr 30, 2026
3691eef
e2e: stub MediaDevices via defineProperty on the property's owner
rubenarslan Apr 30, 2026
02306f6
Add visual_analog_scale item type
rubenarslan May 2, 2026
ee8e9d7
form_v2: port ExpiryNotifier — Page-expired modal
rubenarslan May 2, 2026
4d486c6
form_v2: wipe offline queue on logout / push-subscription change
rubenarslan May 2, 2026
89e243c
e2e: clear stale test.fixme markers, replace with explicit skip
rubenarslan May 2, 2026
e9c7a08
docs: form_v2.md + plan reflect VAS, ExpiryNotifier, queue wipe
rubenarslan May 2, 2026
448d9ef
form_v2: rebuild bundles for VAS + ExpiryNotifier + queue wipe
rubenarslan May 2, 2026
686f3b3
try fixing ultra
rubenarslan May 4, 2026
1bd5092
form_v2: dispatch input+change on VAS hidden input so showif reactivi…
rubenarslan May 4, 2026
95351e5
form_v2: unwrap r(...) on value column before recording into survey_r…
rubenarslan May 4, 2026
3a95668
form_v2: derive Background Sync URL from registration.scope so the
rubenarslan May 4, 2026
d51614e
form_v2: derive next_page from actual renderable pages, not submitted+1
rubenarslan May 4, 2026
09c3582
form_v2: bind queue entries to their unit-session and drop on mismatch
rubenarslan May 4, 2026
91fad1b
form_v2: scope r-call result cache to unit_session_id
rubenarslan May 4, 2026
a3cc2df
Merge branch 'master' into feature/form_v2
rubenarslan May 8, 2026
8cad193
post-merge: reconcile package-lock + rebuild bundles
rubenarslan May 8, 2026
3cfc4ee
e2e: bootstrap PWA_TEST_CODE in globalSetup
rubenarslan May 8, 2026
9425572
e2e: BS iOS context-fixture fix + Android parking + SDK bump
rubenarslan May 8, 2026
3e1ba98
Merge remote-tracking branch 'origin/api_v1_dev' into feature/form_v2
rubenarslan May 21, 2026
bc16dc8
post-merge: rebuild form bundle against merged sources
rubenarslan May 21, 2026
cfe409a
form_v2: long-form iteration + pivoted export + optional dual-write
rubenarslan May 21, 2026
42c3681
Merge remote-tracking branch 'origin/master' into feature/form_v2
rubenarslan May 23, 2026
bab3153
chore(sql): renumber form_v2 patches 047-055 → 057-065
rubenarslan May 23, 2026
7fbe038
fix(form_v2): DB::query() returns array of rows for SELECTs
rubenarslan May 23, 2026
b5dd368
feat(form_v2): solo-layout skin (patch 066)
rubenarslan May 23, 2026
0bb9f34
fix(form_v2): replace Math.random fallback in genUuid with Web Crypto
rubenarslan May 23, 2026
5d282a4
feat(form_v2): default-theme polish pass (save indicator, completion,…
rubenarslan May 23, 2026
46b7a23
feat(form_v2): Typeform solo step-controller, letter-key setting, bot…
rubenarslan Jun 4, 2026
3b99764
test(form_v2): complete all_widgets catalogue + coverage guard
rubenarslan Jun 4, 2026
80eaac5
feat(survey): accept JSON survey export on upload
rubenarslan Jun 4, 2026
fa664c2
fix(form_v2): solo-layout bug pass — scroll, tab order, sliders, auto…
rubenarslan Jun 4, 2026
5948b2e
fix(form_v2): render choices on later pages (mc/select/matrix were em…
rubenarslan Jun 4, 2026
bfd6e5b
feat(form_v2): spec alignment — top labels, group a11y, blur validati…
rubenarslan Jun 4, 2026
1871217
feat(form_v2): rate-limited incremental autosave (persist on advance/…
rubenarslan Jun 4, 2026
42891ee
test(form_v2): e2e for spec functionality + update specs for step con…
rubenarslan Jun 4, 2026
de6a04f
feat(form_v2): record layout per response + monotonic progress (+ e2e)
rubenarslan Jun 4, 2026
49bbf6f
test(form_v2): BrowserStack solo-all-items real-device smoke (iPhone …
rubenarslan Jun 4, 2026
76811f5
feat(form_v2): bfcache-scoped "unsaved changes" guard for autosave
rubenarslan Jun 5, 2026
af0f5a0
docs(form_v2): agent_doc for layout modes + cross-cutting v2 changes
rubenarslan Jun 5, 2026
75f8bbe
feat(survey): soft-delete items instead of cascade-wiping long-format…
rubenarslan Jun 5, 2026
da3d4ab
test(survey): e2e regression for item soft-delete
rubenarslan Jun 5, 2026
25970bc
docs(survey): agent_doc for item soft-delete
rubenarslan Jun 5, 2026
ed7b187
fix(form_v2): solo short-screen/iOS — stop trapping content under fix…
rubenarslan Jun 5, 2026
920bb82
test(form_v2): pixel-geometry harness for solo layout (no-chrome-over…
rubenarslan Jun 5, 2026
804a895
test(form_v2): keyboard-aware geometry diagnostic for solo on real iOS
rubenarslan Jun 5, 2026
44720be
fix(form_v2): solo — stop the unverified-types notice scrolling every…
rubenarslan Jun 5, 2026
4684e0b
fix(form_v2): suppress the unverified-types heads-up in solo layout
rubenarslan Jun 5, 2026
0a7ce8a
fix(form_v2): solo — stop the focused field jumping as the iOS keyboa…
rubenarslan Jun 5, 2026
f7213ef
fix(form_v2): visual_analog_scale showed a visible default thumb
rubenarslan Jun 5, 2026
ddd2c12
feat(form_v2): VAS thumb as a vertical pencil mark; block untouched r…
rubenarslan Jun 5, 2026
814ab3d
fix(bot_check): stop rejecting fast PoW solves ('verified but can't p…
rubenarslan Jun 5, 2026
d326430
feat(bot_check): generous session-bound TTL; customisable copy for co…
rubenarslan Jun 6, 2026
5c6c50b
feat(form_v2): remove the A/B/C letter badges on solo option cards
rubenarslan Jun 6, 2026
2883e94
feat(form_v2): solo — hide the run footer behind a ? toggle next to Back
rubenarslan Jun 6, 2026
b557a5b
fix(form_v2): icon addon layout on email/tel/url/etc. items
rubenarslan Jun 6, 2026
ad34af3
fix(form_v2): gate ALL required items client-side (geopoint, file/ima…
rubenarslan Jun 6, 2026
6a050cc
test(form_v2): required/optional gating sweep across all item types (…
rubenarslan Jun 6, 2026
941ca73
fix(form_v2): solo — keep the focused field above the keyboard (text …
rubenarslan Jun 6, 2026
02b41f5
fix(form_v2): geopoint — robust geolocation (timeout + clear fallback…
rubenarslan Jun 6, 2026
00fa439
test(form_v2): true end-to-end required-gating lane on an all-require…
rubenarslan Jun 6, 2026
0bf7285
wip(bot_check): Altcha + Argon2id migration — INCOMPLETE (subagent in…
rubenarslan Jun 6, 2026
da29790
wip(bot_check): altcha bundling investigation — import external build…
rubenarslan Jun 6, 2026
8afa720
feat(bot_check): Altcha + Argon2id working end-to-end (standalone-scr…
rubenarslan Jun 6, 2026
13f2040
test(bot_check): Altcha e2e + backend smoke green; domain message on …
rubenarslan Jun 6, 2026
1c1d58b
fix(form_v2): solo — stop the keyboard scroll-loop; make OK reliable …
rubenarslan Jun 6, 2026
2d8d6f8
fix(form_v2): icon addons + geopoint render as one flush pill
rubenarslan Jun 8, 2026
710d5ec
docs(form_v2): note v1/v2 asset separation is bundle-level, not selec…
rubenarslan Jun 8, 2026
2d40496
fix(form_v2): make the v2 survey completable end-to-end (solo + non-s…
rubenarslan Jun 8, 2026
567cb87
test(form_v2): BS-aware timeouts + blocked-path diagnostics for v2-fi…
rubenarslan Jun 8, 2026
bd94ee4
fix(form_v2): showif NA is hidden by default (v1 parity), not shown
rubenarslan Jun 8, 2026
557ffe3
test(form_v2): non-solo completes on a real device; robust page-chang…
rubenarslan Jun 8, 2026
8d4d0b0
fix(bot_check): load Altcha's stylesheet so the v2 widget matches the…
rubenarslan Jun 8, 2026
856e926
fix(form_v2): submit big button, VAS extremes, rating_button, Saved p…
rubenarslan Jun 9, 2026
b7c5219
feat(form_v2): full PWA-item implementation (push SW, install prompt,…
rubenarslan Jun 9, 2026
964eca3
test(form_v2): e2e for v2 PWA items (add_to_home, push wiring, reques…
rubenarslan Jun 9, 2026
af098c5
docs(form_v2): xlsx fixtures for PWA items + bot_check, with generato…
rubenarslan Jun 9, 2026
ac9cccc
fix(form_v2): code-review backend fixes
rubenarslan Jun 9, 2026
3d60e8e
fix(form_v2): code-review frontend fixes
rubenarslan Jun 9, 2026
059c060
fix(pwa): manifest references only assets that exist; valid protocol …
rubenarslan Jun 9, 2026
80df6ea
test(e2e)+docs: altcha wrapper clicks, cookie fixme root-caused, layo…
rubenarslan Jun 9, 2026
e83c59a
Bugs found and fixed (all verified live, feature/form_v2 working tree…
rubenarslan Jun 10, 2026
ba8c8c6
chore(patches): renumber form_v2 patches 057-069 → 060-072 to make ro…
rubenarslan Jun 11, 2026
cbcfbcd
Merge master (v1.1.0) into feature/form_v2
rubenarslan Jun 11, 2026
9acff90
fix(parsedown): catch \Throwable at all parsedown->text() call sites
rubenarslan Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions .claude/agents/ui-playwright-tester.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"frontend-design@claude-plugins-official": true
}
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ repomix-output-formr.xml

playwright-report/
test-results/
tests/e2e/artifacts/
tests/e2e/setup/admin-state.json
tests/e2e/setup/pwa-test-code.txt

# Run exports and other prod artefacts that get dropped into tests/e2e/
# for ad-hoc analysis (admin Run-export JSONs, sanitised DB dumps, etc.).
Expand Down
94 changes: 94 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

56 changes: 54 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Surveys are simple or complicated forms and surveys used to gather information i

There is a wide variety of items to choose from: text and number inputs, Likert scales, sliders, geolocation, date pickers, dropdowns and many more. They are geared towards power users, so instead of dragging and dropping elements till your fingers bleed, you upload item spreadsheets that can easily be re-used, combined and shared. The format is loosely based on the [XLSform standard](https://xlsform.org/).

A new "Form" RunUnit (form_v2) renders the same spreadsheets as a single-page AJAX form with reactive `showif`, an offline queue, and PWA support — opt in per-study via `rendering_mode='v2'`. See [`documentation/form_v2.md`](documentation/form_v2.md) for the admin-facing guide and [`plan_form_v2.md`](plan_form_v2.md) for the engineering plan.

## Runs
#### control your study like a boombox
Runs enable you to link surveys and chain them together. Using a number of boombox-themed control elements to control the participant's way through your study, you can design studies of limitless complexity. You can
Expand Down
14 changes: 13 additions & 1 deletion application/Controller/AdminRunController.php
Original file line number Diff line number Diff line change
Expand Up @@ -744,11 +744,21 @@ private function showPanicButton()

private function getUnitAddButtons()
{
return array(
$buttons = array(
'Survey' => array(
'title' => 'Add Survey',
'icon' => 'fa-pencil-square',
),
);

if (Config::get('form_v2_enabled', false)) {
$buttons['Form'] = array(
'title' => 'Add Form (form_v2, beta)',
'icon' => 'fa-wpforms',
);
}

$buttons += array(
'External' => array(
'title' => 'Add External Link',
'icon' => 'fa-external-link-square',
Expand Down Expand Up @@ -786,5 +796,7 @@ private function getUnitAddButtons()
'icon' => 'fa-stop',
),
);

return $buttons;
}
}
77 changes: 73 additions & 4 deletions application/Controller/AdminSurveyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ public function addSurveyAction() {


if ($this->validateUploadedFile($file)) {
// JSON survey export → import via createFromData (the same path
// run-structure imports use). Only reached for new surveys; the
// edit/re-upload path stays spreadsheet-only.
if (strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)) === 'json') {
$data = json_decode(file_get_contents($file['tmp_name']));
if (!is_object($data) || empty($data->name) || empty($data->items)) {
alert('<strong>Error:</strong> That is not a valid survey JSON export (it must have "name" and "items").', 'alert-danger');
delete_tmp_file($file);
return $this->request->redirect('admin/survey');
}
if (SurveyStudy::createFromData($data, array('user_id' => $this->user->id))) {
alert('<strong>Success!</strong> New survey created from JSON!', 'alert-success');
$created = SurveyStudy::loadByUserAndName($this->user, $data->name);
$redirect = ($created && $created->valid)
? admin_study_url($created->name, 'show_item_table')
: 'admin/survey';
} else {
alert('<strong>Bugger!</strong> The survey JSON could not be imported. See the messages above.', 'alert-danger');
$redirect = 'admin/survey';
}
delete_tmp_file($file);
return $this->request->redirect($redirect);
}

$study = new SurveyStudy(null);
if ($study->createFromFile($file)) {
// upload items
Expand Down Expand Up @@ -93,13 +117,19 @@ protected function validateUploadedFile($file, $editing = false) {
return false;
}

// Define the list of allowed extensions
// Define the list of allowed extensions. A .json survey export is
// accepted only when adding a NEW survey (createFromData); the edit /
// re-upload-items path runs the spreadsheet reader, which can't parse it.
$allowedExtensions = array('xls', 'xlsx', 'ods', 'xml', 'txt', 'csv');
if (!$editing) {
$allowedExtensions[] = 'json';
}
// Get the file extension
$fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
// Check if the extension is in the allowed list
if(!in_array($fileExtension, $allowedExtensions)){
alert("<strong>Error:</strong> The format must be one of .xls, .xlsx, .ods, .xml, .txt, or .csv.", 'alert-danger');
$formats = $editing ? '.xls, .xlsx, .ods, .xml, .txt, or .csv' : '.xls, .xlsx, .ods, .xml, .txt, .csv, or .json';
alert("<strong>Error:</strong> The format must be one of {$formats}.", 'alert-danger');
return false;
}

Expand Down Expand Up @@ -542,10 +572,18 @@ private function changeSettings($settings) {
'expire_invitation_after', 'expire_invitation_grace',
'enable_instant_validation', 'hide_results', 'use_paging',
'unlinked', 'google_file_id',
// form_v2 per-study flags. Without these in the allowlist the
// array_intersect_key below would strip them before the v2
// handling block runs and the saves would silently rewrite
// the columns to their false/null defaults.
'offline_mode', 'allow_previous', 'layout',
];
$settings = array_intersect_key($settings, array_flip($allowed));
array_walk($settings, function (&$value, $key) {
if ($key !== 'google_file_id') {
// String-typed columns must opt out of the int cast — google_file_id
// is a Drive ID; layout is an ENUM('default','solo').
$stringCols = ['google_file_id', 'layout'];
array_walk($settings, function (&$value, $key) use ($stringCols) {
if (!in_array($key, $stringCols, true)) {
$value = (int) $value;
}
});
Expand All @@ -568,6 +606,18 @@ private function changeSettings($settings) {
$settings['hide_results'] = (int) (isset($settings['hide_results']) && $settings['hide_results'] === 1);
$settings['use_paging'] = (int) (isset($settings['use_paging']) && $settings['use_paging'] === 1);
$settings['unlinked'] = (int) (isset($settings['unlinked']) && $settings['unlinked'] === 1);
// form_v2 per-study flags — accept only when the study is on the v2
// pipeline, so checkboxes on a v1 study can't silently mutate data the
// v1 renderer doesn't read. offline_mode defaults to 1 (opt-out),
// allow_previous defaults to 0 (opt-in).
if ($this->study->rendering_mode === 'v2') {
$settings['offline_mode'] = (int) (isset($settings['offline_mode']) && $settings['offline_mode'] === 1);
$settings['allow_previous'] = (int) (isset($settings['allow_previous']) && $settings['allow_previous'] === 1);
$layoutInput = $settings['layout'] ?? 'default';
$settings['layout'] = in_array($layoutInput, ['default', 'solo'], true) ? $layoutInput : 'default';
} else {
unset($settings['offline_mode'], $settings['allow_previous'], $settings['layout']);
}

// user can't revert unlinking
if ($settings['unlinked'] < $this->study->unlinked) {
Expand Down Expand Up @@ -601,6 +651,25 @@ private function changeSettings($settings) {
alert('Survey settings updated', 'alert-success', true);
}

/**
* form_v2 compatibility scan: classify each showif/value expression on the
* current study as empty / r-wrapped / JS-OK / needs-r()-wrap. Rendered on
* the survey settings page (survey/index) via the "Run v2 compatibility
* scan" button — surfaced only for v2 studies. Reuses FormV2CompatScanner
* so the admin view and the CLI (bin/form_v2_compat_scan.php) agree.
*/
private function formV2CompatScanAction() {
if (!$this->study || !$this->study->valid) {
formr_error(404, 'Not Found', 'Study not found');
}
$report = FormV2CompatScanner::scan($this->study->id);
$this->setView('survey/form_v2_compat_scan', [
'study' => $this->study,
'report' => $report,
]);
return $this->sendResponse();
}

private function setStudy($name) {
if (!$name) {
return;
Expand Down
9 changes: 8 additions & 1 deletion application/Controller/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,14 @@ protected function getJsConfig() {
if ($cookieconsent && ($decoded = json_decode($cookieconsent, true))) {
$config['cookieconsent'] = $decoded;
}

// form_v2 offline-queue blob cap. Hardcoded to 10MB in the bundle
// until now; expose via instance config so deployments running
// long-form audio/video can raise it. Per-study override is a
// future enhancement (would mean a column on survey_studies).
$config['form_v2'] = array(
'offline_blob_max_mb' => (int) Config::get('form_v2_offline_blob_max_mb', 10),
);

return $config;
}

Expand Down
Loading