23 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, app_repo_head_before, app_repo_head_after, plugin_repo_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | plan_head_before | plan_head_after | app_repo_head_before | app_repo_head_after | plugin_repo_head_after | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-domain-jobs-and-external-integrations | 05 | feedback |
|
|
|
|
|
7eb0174612 |
b5d20b3bfd |
eea0b1607ef9b5d14b3a5186bf322e6eb45a4b9b | 9fc38d9 | af5d77c |
|
|
|
|
|
100min | 2026-10-04 | complete |
Phase 14 Plan 05: sm-feedback-plugin Summary
A new shared plugin, sm-feedback-plugin, ports the WinterCMS feedback widget to Go. The embedded widget loads its configuration, a visitor sends a report with a screenshot, and a signed-in collector hides the widget, all as PHP answers, behind the same key and Origin gate. Each report reaches G15Office as a task with its screenshot, in the requests PHP sent. Operators configure the widget and read the reports in the admin.
Performance
- Duration: about 100 min
- Started: 2026-10-03T21:44:47Z
- Completed: 2026-10-04T00:25Z
- Tasks: 3
- Commits: 3 in sm-feedback-plugin (pushed), 7 in fonoteka.go, 1 code commit in summercms.go (plus this summary)
- Files: 37 in the plugin (+5430), 52 in fonoteka.go (+1178/-27), 4 in summercms.go (+106/-2)
Accomplishments
- sm-feedback-plugin (D-12). The empty remote was cloned at
plugins/golem15/feedback, built, committed and pushed, then registered withgit submodule add.masteris ataf5d77cand in sync with origin. fonoteka.go loadsgolem15.feedbackaftergolem15.userand beforegolem15.fonoteka:go.work,go.mod(require and replace),summer.yaml, the regeneratedplugins.gen.goandapp.PluginIDsall name it. - Widget config (D-13).
GET /_feedback/api/v1/{key}/configchecks the key in constant time and answers 404Not foundwhen the widget is disabled, has no key or gets the wrong one. The Origin gate lets a request with no Origin through. A request whose Origin host, lower-cased, is not on the list gets 403Origin not allowed, and an empty list refuses every Origin.models.URLHostfollows PHP'sparse_url, includinglocalhost:3000and IPv6 brackets.?lang=enselects the English labels. - Submit (D-13).
POST {key}/submitvalidates with PHP's rules and returns the 422 envelope with Laravel's messages in rule order.- The screenshot must pass a copy of ImageContentGuard, or the answer is 422
The file is not a valid image.. - One transaction stores the submission, queues the G15Office job and stores the public screenshot (attachOne
screenshot). A failed write keeps no row and no job. The answer is 202{"success":true}.
- Hiding the widget. JWT
PUT me/hiddenaccepts Laravel booleans, writes only the caller's row and answers{"hidden":bool}. AGetApiArrayEventlistener putsfeedback_widget_hiddenon every golem15.user payload. The recorded me/hidden cases include a followingGET /_user/api/v1/fetch, so the payload key is asserted against PHP. - Preflight. surf's CORS layer answers
OPTIONSon_feedback/api/*, and no route is registered for it. surf now sends the headers PHP sends (see Deviation 1). - G15Office (D-13, D-15).
classes.SyncG15Officebuilds PHP's title.Str::limitis ported withmb_strwidthwidths, using a table generated from PHP. The whitespace collapse is PCRE\s, so it includes VT.- It builds PHP's Markdown description, the type map and the settings' status and priority.
- It posts the task JSON, then the screenshot as the multipart part
file. Both requests go throughfetchguardin AllowHostsMode for the base URL's host, withAccept: application/json, the Bearer token and no User-Agent. - Success stores
sentand the task id. Failure storesfailedand PHP's message, and returns the error so River retries, up toconga.MaxAttempts(3). When unconfigured, nothing is sent. - PHP ran the job under the sync queue through the recording proxy. Its exchange is replayed offline. A one-word mutation of the title limit failed the replay.
- Admin and assets.
- The settings screen (code
feedback) uses cabana's field types only. The colors are text fields, the widget key is read-only throughattributes, and the defaults areinitSettingsData. A row saved without a key gets a new one. - The submissions list is read-only: no form and no buttons, every column plain text.
- The plugin registers the
golem15.feedback.manage_settingspermission (developer role) and a Feedback menu. embed.jsis served byte for byte, with an ETag.feedback:import-settingscopies the WinterCMS row once, the widget key included. Without--forceit refuses settings that were already changed.
- The settings screen (code
- Parity.
- 28 cases were recorded against PHP: 10 config, 9 submit, 2 OPTIONS and 7 me/hidden.
- The submit cases include each limit at the boundary and one past it (5000-character multibyte message, page_url, user_agent, console_log), the rule-order 422, a non-image, a spoofed JPEG, a foreign Origin and a wrong key.
- The config cases include an uppercase Origin,
localhost:3000,null, an empty list, a disabled widget and an unconfigured widget. - The corpus has 175 routes: 172 ported and passing, and 3 pending (D-09).
check_corpus --require-recorded --check-secretsis green.
Task Commits
sm-feedback-plugin (git@git.golem15.com:golem15/sm-feedback-plugin.git, master, pushed):
b4b4135feat: golem15.feedback plugin with the widget config route (Task 1)92e1b4efeat: feedback submissions with screenshots, the hide-widget preference and its user payload key (Task 2)af5d77cfeat: G15Office job, widget settings and submissions admin, embed.js and the settings importer (Task 3)
fonoteka.go (not pushed):
a29c809chore(14-05): mount sm-feedback-plugin at plugins/golem15/feedback (.gitmodules+ gitlink only)a845178feat(14-05): the application loads golem15.feedback after golem15.user (Task 1)2028880feat(14-05): the embedded widget loads its configuration from golem15.feedback exactly as from PHP (Task 1)e497e94chore(14-05): bump sm-feedback-plugin (gitlink only, Task 2)8d5d451feat(14-05): a visitor submits feedback, a collector hides the widget and the user payload reports it, as in PHP (Task 2)86afa59chore(14-05): bump sm-feedback-plugin (gitlink only, Task 3)9fc38d9feat(14-05): the team receives each submission as a G15Office task with its screenshot, as PHP sent it (Task 3)
summercms.go:
b5d20b3fix(14-05): surf answers OPTIONS on CORS paths with Laravel's HandleCors headers (README and routing docs updated; TestDocsTree and docs:build --check pass)
Tracer gate: after Task 1 the <verify> was re-run end to end (vet, the named tests with -race, the corpus, check_corpus, submodule status) and passed. The expansion tasks then went ahead.
Deviations from Plan
Auto-fixed issues
1. [Rule 1 - Bug, framework] surf's OPTIONS answer did not match PHP
- Found during: Task 1 recording
- Issue: PHP's preflight answers 204 with
Access-Control-Allow-Methods: POSTandAccess-Control-Allow-Headers: content-type(php-cors echoes the request when*allows any),Cache-Control: no-cache, privateandContent-Type: text/html; charset=UTF-8. A plain OPTIONS answers 204 with the Cache-Control. surf sent*for both Allow headers and no Cache-Control or Content-Type, so the plan's assumption that "the recorded preflight cases must replay green" failed. - Fix:
surf.writeOptionssends what HandleCors sends. A configured method or header list is still sent as configured. AddedTestCORSOptionsMatchesLaraveland updated the surf README anddocs/services/routing.md. - Commit:
b5d20b3(summercms.go)
2. [Rule 3 - Blocking] The Origin gate could not be replayed
- Issue:
capture-rules.yamlkeeps only Authorization, Content-Type, Accept and Accept-Language, so a recorded Origin would be dropped and a 403 case would replay as 200. - Fix: The rules also keep
Origin,Access-Control-Request-MethodandAccess-Control-Request-Headers. Existing fixtures are unaffected. - Commit: 2028880
3. [Rule 2 - Privacy] The PHP checkout's .env could point the job at a real G15Office
- Fix:
php_parity.shexportsG15_OFFICE_BASE_URL,G15_OFFICE_TOKENandG15_OFFICE_PROJECTempty unless the caller sets them. The job recording sets them tohttps://office.parity.testand a fake token, behind the scripted proxy. - Commit: 2028880
4. [Rule 1 - Bug] The fonoteka seed left alice's user flags to chance
- Issue: The me/hidden user-payload step expected
has_self_set_password: true. The shared replay database left the flag at whatever an earlier user-api replay had set. - Fix:
seedFonotekaCasenow sets alice'shas_self_set_password,must_change_password,marketing_consentandis_onboardedto PHP's values. No other fonoteka-seeded fixture shows them. - Commit: 8d5d451
5. [Rule 3 - Blocking] TestParityContract rejected the new ported routes
- Fix: Added the
feedbackSeedRoutesset. - Commit: 2028880
Plan details refined
6. All 28 route cases were recorded in one session and committed with Task 1. The submit, OPTIONS and me/hidden routes stayed pending until Task 2 flipped them. The routes were recorded with QUEUE_CONNECTION=database: under sync, PHP runs the job inside the request, and an unconfigured G15Office would make the submit a 500.
7. The settings screen's code is feedback, not WinterCMS's settings. cabana keys settings screens by code across all plugins, so a generic code would clash with the next shared plugin.
8. The job goes through conga.Dispatch, with a summer_jobs row labelled with the PHP job class, and is completed on success. Inside the one transaction it is queued before the screenshot is written, so TestFeedbackSubmit/rolled-back-write-queues-nothing proves that a failed screenshot write leaves no job behind. The order is not visible outside the transaction.
9. No models/submission/fields.yaml was created. The list is read-only and has no form, which TestFeedbackAdminSchemas asserts.
10. TestImageGuardCopy compares the copy with the framework guard (attach.IsAllowedImage) and with PHP's verdicts on the recorded fixtures. A plugin in its own repository cannot import the application's fonoteka guard.
11. The plugin replays the G15Office sidecar from its own classes/testdata copy. The application keeps the recorded original under fixtures/jobs, with a rows golden (required by TestUpstreamSidecarsAreReplayed), and TestFeedbackJobSidecarMatchesPlugin keeps the two copies byte-identical.
12. The G15Office client uses AllowHostsMode, so it accepts an https base URL only. PHP's curl would also post over http.
13. All string columns are TEXT. PHP's user_agent is string(255), but its own rule allows 500 characters, which Postgres would refuse.
14. The submit route has nine cases, because the Go replay keeps one app per route and the bucket allows ten. TestFeedbackSubmit/throttle asserts that the 11th post gets a 429.
15. Extra tests beyond the plan: TestKeyMatches, TestURLHost (PHP parse_url vectors), TestAllowedOriginHosts and TestTaskText (Str::limit and buildTitle/buildDescription against PHP outputs).
16. The fonoteka.go README layout table now lists the golem and feedback submodules. The golem row was missing since 14-04.
17. Commits land on master in all three repos, as in the earlier Phase 14 plans (branching_strategy: none, sequential executor). gsd-tools reports master as protected and allow_default_branch_commits is unset; the orchestrator's sequential-execution instructions were followed.
Total deviations: 17. Five are auto-fixes and twelve are refinements. Impact: the only framework change is surf's OPTIONS answer. It is documented and limited to OPTIONS requests on CORS paths. There is no scope creep.
Issues Encountered
/tmphit its disk quota. The parity root moved to~/.cache/summercms-parity/p1405, and Go and PHP temporary files to~/.cache/gotest-tmp. Older caches in/tmpleft by earlier sessions were not touched.- A spec with
?lang=eninsidepath:was sent with an escaped path and got Winter's 404 page. It was re-recorded withquery:. artisan servekeeps a childphp -Salive after the parent is killed. The child was stopped by its PID.- The submit recordings wrote two screenshots into the PHP checkout's
storage/app/uploads/public/6ac, and that directory was removed.git statusin the PHP checkout shows only the user's own pre-existing files.
Verification
- sm-feedback-plugin:
go vet ./...andgo test ./...pass both standalone (GOWORK=off) and in the workspace. Every named test passes with-race. - fonoteka.go:
go vet ./...passes,go test ./... -count=1passes (the app and parity packages), andgo test ./... -shortpasses. The fonoteka, golem and user plugin suites pass with-count=1. - Parity:
TestParityCorpusreportsrecorded 175/175 passing 172 failing 0 unrecorded 0 pending 3.TestCheckCorpusPortedCaseStatus,TestParityContract,TestSchemaMatchesPHPSnapshot,TestUpstreamSidecarsAreReplayedandTestFeedbackJobSidecarMatchesPluginpass.check_corpus --routes … --require-recorded --check-secretspasses with 175/175. - summercms.go:
go vet ./...andgo test ./... -count=1pass.TestDocsTreeandsummer docs:build --checkpass. - Acceptance greps for all three tasks pass:
.gitmoduleshas 1 feedback path, and the plugin'sorigin/masterresolves.feedbackRouteIDsappears 3 times, androutes.snapshothas 4_feedback/api/v1lines.expectedRouteCountandexpectedPHPRoutesare 175, andexpectedPortedRoutesis 172.ConstantTimeCompareappears once, andfeedback_widget_hiddenis in plugin.go.routes.gohas noOptions(.- The embed.js sha256 values are identical, and fields.yaml has no
colorpicker. - The job sidecar's Authorization is
Bearer {{secret:g15office-token}}, and jobs.go hasconga.MaxAttempts(3).
Known Stubs
None.
Threat Flags
None. The new surfaces are in the plan's threat register:
- The public widget routes (T-14-30, T-14-31), with the constant-time key check, the fail-closed Origin list and the per-IP buckets.
- The screenshot (T-14-32), with the rules and the sniff-and-decode guard.
- The G15Office token and forwarding (T-14-33, T-14-34). The token comes from config only, the sidecars mask it, the client is limited to the configured host, and the job logs only the submission id.
- me/hidden (T-14-35), which writes only the principal's row.
- The admin list (T-14-36), which renders text columns only.
The embed.js route serves an embedded static file and reads no input. No key, token or DSN literal was pushed: the diff was scanned before each push, and the tests use only short fake secrets.
User Setup Required
None for development. At cutover (Phase 15) the operator:
- sets
SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL(https),__TOKENand__PROJECT; - runs
feedback:import-settingsonce, so embed snippets keep their widget key; - runs a queue worker that serves the
feedbackqueue.
Next Phase Readiness
- 14-06 picks up the unit-test gate. It should add
./plugins/golem15/feedback/...tocheck-phase14.sh's plugin list and mark API-08 (left Pending here because 14-06 also declares it).
Phase: 14-domain-jobs-and-external-integrations Completed: 2026-10-04
Self-Check: PASSED
Every listed file exists; commits b5d20b3 (summercms.go), a29c809, a845178, 2028880, e497e94, 8d5d451, 86afa59 and 9fc38d9 (fonoteka.go) and b4b4135, 92e1b4e and af5d77c (sm-feedback-plugin, on origin/master) are present. The final go test ./... -count=1 in fonoteka.go passed after the last commit.