docs(32): add code review fix report
This commit is contained in:
490
.planning/phases/32-seo-foundation/32-REVIEW-FIX.md
Normal file
490
.planning/phases/32-seo-foundation/32-REVIEW-FIX.md
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
phase: 32-seo-foundation
|
||||
fixed_at: 2026-10-27T14:47:00Z
|
||||
review_path: /media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/phases/32-seo-foundation/32-REVIEW.md
|
||||
iteration: 1
|
||||
findings_in_scope: 29
|
||||
fixed: 28
|
||||
skipped: 1
|
||||
status: partial
|
||||
---
|
||||
|
||||
# Phase 32: Code Review Fix Report
|
||||
|
||||
Two fixer lanes ran in parallel on disjoint repositories. CR-04 was split: the framework half (WidgetField.vue element ordering) and the plugin half (element action queue with dropped-action retry); both are fixed. Skipped: WR-03 (needs a design decision: a way to mark a failing extension's fields unavailable so the admin does not post them back).
|
||||
|
||||
---
|
||||
|
||||
## Lane A: summercms.go and sm-journal-plugin
|
||||
|
||||
|
||||
### Phase 32: Code Review Fix Report (lane A + framework half of CR-04)
|
||||
|
||||
**Fixed at:** 2026-10-27T15:55:00Z
|
||||
**Source review:** .planning/phases/32-seo-foundation/32-REVIEW.md
|
||||
**Iteration:** 1
|
||||
|
||||
The scope was CR-01, CR-02, WR-01..WR-07 (lane A), plus the framework half of CR-04 (WidgetField.vue). The sm-seo-plugin half of CR-04 was out of scope and not touched.
|
||||
|
||||
**Summary:**
|
||||
- Findings in scope: 10
|
||||
- Fixed: 9 (6 commits in summercms.go, 3 in sm-journal-plugin)
|
||||
- Skipped: 1 (WR-03, needs a design decision)
|
||||
|
||||
**Verification ran in the main checkout.** `workflow.use_worktrees` is false, so no worktree was used.
|
||||
- summercms.go: `go vet ./...` clean; `go test -short ./...` green; Docker suites `go test ./modules/cabana ./modules/signpost` green; `examples/hello` green.
|
||||
- admin: `npm --prefix admin run typecheck`, `npm --prefix admin run test` (1564 tests) and the build all pass. `scripts/check-admin-dist.sh` passes.
|
||||
- docs: `summer docs:build --check` reports no problems.
|
||||
- release gates: `scripts/check-phase32.sh --marker --additive` passes, with +0 exported API lines in cabana, pact and signpost against v0.5.1.
|
||||
- sm-journal-plugin: `go vet ./...` clean; `go test ./...` (Docker) green.
|
||||
|
||||
Nothing was tagged or pushed.
|
||||
|
||||
### Fixed Issues
|
||||
|
||||
### CR-01: `required: true` on a form extension field makes every save of the host form fail
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** summercms.go
|
||||
**Commit:** 676ca53
|
||||
**Files modified:** `modules/cabana/crud.go`, `modules/cabana/form_extensions.go`, `modules/cabana/form_extensions_postgres_test.go`, `docs/backend/admin-controllers.md`
|
||||
**Applied fix:**
|
||||
- `mergedRules` skips extension fields, so the owner's model never gets a rule for them.
|
||||
- `liftExtensionValues` checks `required` on the lifted value, and only for visible extensions:
|
||||
- On create, the value must be sent and not blank. Blank means null or whitespace; for a translatable field it means the default locale's text.
|
||||
- On update, the value is checked only when it is sent.
|
||||
- New Postgres test `TestFormExtensionRequired` covers both cases:
|
||||
- An administrator who sees the extension gets a 422 when the value is missing, blank or null, a 201 when it is filled, and a 200 on a partial update.
|
||||
- An administrator who does not see the extension can create and update freely.
|
||||
- Docs bullet added.
|
||||
|
||||
### CR-02: Plugin field-type upload route ignores the extension's Permissions/Visible
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** summercms.go
|
||||
**Commit:** 5471b66
|
||||
**Files modified:** `modules/cabana/field_type_upload.go`, `modules/cabana/form_extensions_postgres_test.go`, `docs/backend/admin-controllers.md`
|
||||
**Applied fix:** `fieldUpload` answers 404 when the field belongs to an extension that `extensionVisible` refuses. New Postgres test `TestFormExtensionUpload`:
|
||||
- A visible upload gets a 201 on both create (id 0) and update.
|
||||
- An administrator without the extension's permission gets a 404 on both, and `Upload` never runs.
|
||||
|
||||
The docs now say the extension's "field type uploads" answer 404 too. I found no other route that resolves an extension field by name without a visibility check.
|
||||
|
||||
### CR-04 (framework half): WidgetField assigns `element` after `append`
|
||||
|
||||
**Status:** fixed (framework half only; the sm-seo-plugin element changes belong to the other fixer)
|
||||
**Repo:** summercms.go
|
||||
**Commit:** d660cd0
|
||||
**Files modified:**
|
||||
- `admin/src/components/form/fields/WidgetField.vue`
|
||||
- `admin/tests/form/WidgetField.test.ts`
|
||||
- `docs/backend/partials-and-widgets.md`
|
||||
- `modules/boardwalk/dist/index.html`
|
||||
- `modules/boardwalk/dist/assets/index-*.js` and `editor-*.js` (renamed by the rebuild)
|
||||
|
||||
**Applied fix:**
|
||||
- `element = created` now runs before `mountPoint.value.append(created)`.
|
||||
- New vitest case (`an action from connectedCallback (CR-04)`) mounts attached to the document. It uses an element that dispatches `summer-action` with a payload from `connectedCallback`, then checks that the POST is sent with that payload, that `busy` is set and then cleared, and that `summer-result` is dispatched. I confirmed the case fails on the old ordering.
|
||||
- Dist rebuilt and committed; `check-admin-dist.sh` passes.
|
||||
- Docs note that an action dispatched from `connectedCallback` is taken, and that one dispatched while `busy` is set is dropped.
|
||||
|
||||
### WR-01: Admin writes that change or delete records fire no record event
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** summercms.go
|
||||
**Commit:** 468af06
|
||||
**Files modified:**
|
||||
- `modules/cabana/relation.go`
|
||||
- `modules/cabana/deferred.go`
|
||||
- `modules/cabana/crud.go`
|
||||
- `modules/cabana/http.go`
|
||||
- `modules/cabana/relation_child.go`
|
||||
- `modules/cabana/record_events_postgres_test.go`
|
||||
- `modules/cabana/README.md`
|
||||
- `docs/backend/admin-controllers.md`
|
||||
|
||||
**Applied fix:**
|
||||
- New `RelationService.saveLinkedChild`: snapshot, mutate, `Save`, then `recordChanges` and `emitChildSaved`. It is used by:
|
||||
- hasMany `linkRelated` and `unlinkRelated`;
|
||||
- the deferred commit's attach of children created in the session (`applyRelationBinding`).
|
||||
- `unlinkDeferred` fires `RecordDeleted` after it deletes a child the session created.
|
||||
- `CRUDService` gains an unexported `reg *Registry` field, set in `crud()`, so the deferred commit's `RelationService` can name the child's controller. It also now carries `app`. The change is additive and unexported.
|
||||
- New subtests `hasMany link and unlink` and `deferred children` in `TestRecordEvents`.
|
||||
- README and docs list the new event sources.
|
||||
|
||||
### WR-02: `RecordSaved.Locale` is the raw Accept-Language header / UI language
|
||||
|
||||
**Status:** fixed (requires human verification of the chosen meaning)
|
||||
**Repo:** summercms.go
|
||||
**Commit:** 1583651
|
||||
**Files modified:**
|
||||
- `modules/cabana/record_events.go`
|
||||
- `modules/cabana/record_events_test.go`
|
||||
- `modules/cabana/relation_child.go`
|
||||
- `modules/cabana/reorder.go`
|
||||
- `modules/pact/record_events.go`
|
||||
- `modules/pact/README.md`
|
||||
- `docs/plugins/extending.md`
|
||||
|
||||
**Applied fix:**
|
||||
- New `eventLocale(ctx)` returns `primaryLanguage(towel.Locale)`. Admin saves, child writes and reorder now all report it, so a raw header like `pl-PL,pl;q=0.9` becomes `pl-PL`, and reorder no longer reports a different, defaulted code.
|
||||
- The pact doc now defines `Locale` as the primary tag of the request locale. It states that an admin save writes the row in the default content locale and that per-locale work should key on `RecordChange.Locale`.
|
||||
- New unit test `TestRecordEventLocale`.
|
||||
|
||||
I deliberately did not take the reviewer's alternative of switching `Locale` to the default content locale: that changes a field's meaning inside a patch release. No consumer reads `ev.Locale` today; I checked sm-seo-plugin, sm-sitemap-plugin and sm-journal-plugin. A human should confirm that "request locale, normalised" is the meaning we want.
|
||||
|
||||
### WR-04: Journal API writes can commit changes (including a slug) and fire no RecordSaved
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** sm-journal-plugin
|
||||
**Commit:** 62c75b1
|
||||
**Files modified:** `controllers/api/posts.go`, `controllers/api/record_events.go`, `record_events_test.go`, `README.md`
|
||||
**Applied fix:**
|
||||
- `Store` and `Update` run `Create`/`Save`, `applyPostAssocs` and `loadPostAssocs` in one `lagoon.Transaction`, with `cabana.EmitRecordSaved(ctx, app, tx, ...)` inside it. The event now follows the commit, and a failure rolls back the row.
|
||||
- The site-cache purge still runs after the commit.
|
||||
- New test step: a PUT with a new slug and an invalid `tags` value returns 500, fires no event and leaves the slug unchanged. It fails without the fix: the slug was `api-lost`.
|
||||
- README updated.
|
||||
|
||||
### WR-05: Journal `postSavedEvent` reports `Old: nil` for a translated `content_html` that existed
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** sm-journal-plugin
|
||||
**Commit:** 7a03640
|
||||
**Files modified:** `controllers/api/record_events.go`, `record_events_test.go`
|
||||
**Applied fix:**
|
||||
- `submittedTranslations` lists `content_html` whenever a translation sends a non-blank `content`. This mirrors `classes.FormatTranslation`, so the snapshot and the event cover the same fields whether or not the body has been mutated yet.
|
||||
- New test step: updating a pl `content` reports the pl `content_html` change with the old HTML as `Old`. It failed before the fix, with `Old` nil.
|
||||
|
||||
### WR-06: Tags auto-created by the Journal post API fire no RecordSaved
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** sm-journal-plugin
|
||||
**Commit:** a6fc7cd
|
||||
**Files modified:** `controllers/api/posts.go`, `controllers/api/posts_helpers.go`, `controllers/api/record_events.go`, `record_events_test.go`, `README.md`
|
||||
**Applied fix:**
|
||||
- `resolveTagIDs` and `applyPostAssocs` now return the tags created by name. These are unexported signature changes.
|
||||
- The handlers emit `tagSavedEvent` (`Created`, with name and slug changes) for each new tag inside the post write's transaction.
|
||||
- New test step: a PUT with a new tag name fires two events, the post and a `journal-tag` `Created`; reusing that tag fires only the post event.
|
||||
- README updated.
|
||||
|
||||
### WR-07: `Hide` leaves the owner's relation-manager and partial endpoints of a hidden field reachable
|
||||
|
||||
**Status:** fixed
|
||||
**Repo:** summercms.go
|
||||
**Commit:** 0e187cf
|
||||
**Files modified:** `modules/cabana/form_extensions.go`, `modules/cabana/form_extensions_smoke_test.go`, `modules/pact/capabilities.go`, `docs/backend/admin-controllers.md`
|
||||
**Applied fix:**
|
||||
- `dropHiddenFields` now receives the owner's original field list and works on copies of the maps. It drops:
|
||||
- the relation whose `fieldName` is hidden. This closes the panel, link, unlink, child CRUD and deferred-commit routes, because `relationOf` and `commitDeferred` read `cc.Relations`.
|
||||
- a hidden partial field's path, from `formPartials` and `partials`. A path that is still used by the list header partial, the preview header partial or another owner partial field keeps rendering.
|
||||
- New smoke test `TestFormExtensionHideEndpoints` checks that `relationOf` refuses the hidden relation-manager relation and that the hidden partial answers 404. With no `Hide`, both still work. It fails without the fix.
|
||||
- The pact `Hide` doc and the docs updated.
|
||||
|
||||
### Skipped Issues
|
||||
|
||||
### WR-03: A failing extension `Load` (or plugin-type `Load`) after a successful save rolls back the whole save
|
||||
|
||||
**Status:** skipped
|
||||
**File:** `modules/cabana/crud.go:980`, `modules/cabana/form_extensions.go` (`hydrateFormExtensions`)
|
||||
**Reason:** Both suggested fixes have problems that need a design decision:
|
||||
- **Log and omit the extension's keys.** This risks data loss. The admin SPA's `editablePayload` always sends translatable fields (`localeRecord(undefined)`), so a response without them makes the next save post empty locale maps to the extension's `Save`.
|
||||
- **Run hydration after the commit.** This stops the rollback, but the request still answers 500 after a committed write. Show still fails for the same `Load`, so editing stays blocked either way.
|
||||
|
||||
A proper fix needs an "extension unavailable" state in the schema/record (for example, the failing extension's fields marked read-only or failed, so the SPA does not post them). That is beyond a v0.5.2 patch.
|
||||
|
||||
**Original issue:** `hydrateFormExtensions` runs inside the save transaction, so one plugin's read-side failure turns a successful save into a 500 and rolls it back. `ShowRecord` likewise fails the whole record.
|
||||
|
||||
|
||||
_Fixed: 2026-10-27T15:55:00Z_
|
||||
_Fixer: Claude (msd-code-fixer)_
|
||||
_Iteration: 1_
|
||||
|
||||
---
|
||||
|
||||
## Lanes B and C: sm-seo-plugin
|
||||
|
||||
|
||||
### Phase 32: Code Review Fix Report (lanes B and C, sm-seo-plugin)
|
||||
|
||||
**Fixed at:** 2026-10-27T14:45:18Z
|
||||
**Source review:** /media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/phases/32-seo-foundation/32-REVIEW.md
|
||||
**Iteration:** 1
|
||||
**Repository:** /media/nvme/dev/golem15/summercms.io/summercms/sm-seo-plugin (branch `main`, on top of b19cd06; not pushed, not tagged)
|
||||
|
||||
**Summary:**
|
||||
- Findings in scope: 20 (CR-03, CR-04 plugin half, WR-08 to WR-25)
|
||||
- Fixed: 20 (WR-15 is a documentation fix only; see its section)
|
||||
- Skipped: 0
|
||||
|
||||
**Verification (run in the main checkout of sm-seo-plugin; `workflow.use_worktrees` is false):**
|
||||
- `go vet ./...`: clean.
|
||||
- `go test -short ./...`: ok.
|
||||
- `go test -count=1 ./...` (Docker, testcontainers Postgres 16): ok.
|
||||
- `npm run typecheck`, `npm test` (77 tests in 7 files), `bash scripts/check-dist.sh`: pass.
|
||||
- `gofmt -l`: clean.
|
||||
- Commits follow `fix: <summary> (<ID>)`, each staged by explicit path, with no co-author trailers.
|
||||
|
||||
### Fixed Issues
|
||||
|
||||
### CR-03: Exact redirects with a trailing slash or upper-case query characters never match at runtime
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 2d81c26
|
||||
**Files:** `updates/202610280001_add_golem15_seo_redirects_source_key.go` (new migration), `classes/redirects.go`, `redirects_test.go`
|
||||
**Applied fix:** The new migration `202610280001_add_golem15_seo_redirects_source_key` adds `source_key`, a stored generated column. It is the SQL form of `exactKey`: the path lowercased, one trailing slash dropped (the root stays `/`), and the query kept as written. A partial index covers it `WHERE match = 'exact'`. `LoadRedirectSet` now fetches with `source_key IN keys`. Postgres computes the column, so every write path fills it (admin, API, batch, import, undo, address changes) and no released migration was edited. The column is not in the GORM model.
|
||||
**Test:** the DB-backed `TestExactRedirectKeys`:
|
||||
- `/old-page/` fires for both `/old-page` and `/old-page/`.
|
||||
- `/list?Page=2` fires for its own query and not for `/list?page=2`.
|
||||
- An ignore-case `/Mixed/Case/` fires for `/mixed/case`.
|
||||
- The generated keys are asserted for each source, including `/` and `/A/?Q=1`.
|
||||
|
||||
With the old `lower(source)` fetch the test fails.
|
||||
|
||||
### CR-04: The SERP preview and the robots.txt tester deadlock (plugin half only)
|
||||
|
||||
**Status:** fixed (plugin half; `WidgetField.vue` was not touched, as instructed)
|
||||
**Commit:** 3ecbd1f
|
||||
**Files:** `assets/src/elements/actions.ts` (new), `serp.ts`, `robotstester.ts`, `labels.ts`, `assets/dist/seo.js`, `tests/elements/host.ts` (new), `tests/elements/serp.test.ts`, `tests/elements/robotstester.test.ts` (new)
|
||||
**Applied fix:** Both elements now share a new `ActionQueue`:
|
||||
- **Drop detection:** after `summer-action` is dispatched, a missing `busy` attribute means the host dropped the action. `inflight` is cleared and the payload is retried once on the next task.
|
||||
- **Second drop:** reported as a failure. The SERP preview shows `contextFailed`; the tester shows a new `testFailed` label (en/pl).
|
||||
- **First request:** `{op:'context'}` is sent on a microtask after `connectedCallback`.
|
||||
- **Disconnect:** clears the request in flight and any pending retry. On reconnect the context is requested again, or the duplicate check is re-armed.
|
||||
|
||||
**Tests:**
|
||||
- `tests/elements/host.ts` is a fake of `WidgetField.onAction`. It sets `busy` synchronously, settles asynchronously, sets `state=error` on failure and removes `busy`. It supports both wiring orders (`append-first` is the v0.5.1 host), dropping N actions, and an unwired host.
|
||||
- The SERP and tester suites cover:
|
||||
- the append-first host;
|
||||
- a dropped retry;
|
||||
- a host that never takes the action;
|
||||
- a queue behind `busy`;
|
||||
- `state=error` unblocking the next request;
|
||||
- a disconnect while a request is in flight.
|
||||
- Against the old `serp.ts`, 6 of the 7 new SERP host tests fail.
|
||||
|
||||
### WR-08: A query-specific exact redirect is silently shadowed by the path-only rule
|
||||
|
||||
**Status:** fixed (requires human verification: matching-order logic)
|
||||
**Commit:** 58b2504
|
||||
**Files:** `classes/redirects.go`, `classes/redirect_graph_test.go`, `README.md`
|
||||
**Applied fix:** `exactKeys` now returns `path?query` before `path`, so the more specific exact rule wins. A unit test covers both insertion orders, and the README states the precedence.
|
||||
|
||||
### WR-09: `AddressChanged` writes redirects without validation or the loop simulation
|
||||
|
||||
**Status:** fixed (requires human verification: redirect-graph logic)
|
||||
**Commit:** cb614cb
|
||||
**Files:** `classes/addresses.go`, `redirects_test.go`, `README.md`
|
||||
**Applied fix:**
|
||||
- **Address form:** absolute addresses on the site URL become paths, and the candidate `old → new` must pass `Problems`. A new address that is not a path on the site creates no redirect and logs a warning.
|
||||
- **Shadowing rules:** step 1 now deletes every exact rule that `exactMatches` the new address with its case and slash options, not only an exact literal match.
|
||||
- **Loop check:** steps 2 to 4 (create or update, then re-point) are simulated in memory with `checkRedirectRules` before anything is written. On a loop, no redirect is written, a warning is logged, and only the entry URL moves.
|
||||
- **Entry URL:** still stored as the descriptor wrote it, so later address diffs keep working.
|
||||
|
||||
**Test:** the DB test `TestAddressChangeGuards` covers:
|
||||
- the pattern-loop case;
|
||||
- a case or slash variant that is removed;
|
||||
- an absolute old address;
|
||||
- a `//evil` new address.
|
||||
|
||||
### WR-10: The save-time loop check races with concurrent writes
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** c2f136a
|
||||
**Files:** `classes/redirect_graph.go`, `classes/addresses.go`, `classes/batch.go`, `redirects_test.go`, `README.md`
|
||||
**Applied fix:** `lockRedirectWrites` runs `SELECT pg_advisory_xact_lock(...)` and is taken in three places:
|
||||
- in `CheckRedirect` before it reads the rules (this covers `SaveRedirect`, the admin extension, undo and the 404 actions);
|
||||
- in `applyAddressRedirects`;
|
||||
- once at the start of every batch, which also covers imports.
|
||||
|
||||
**Test:** `TestConcurrentRedirectWrites` holds A → B uncommitted and checks that a concurrent B → A waits, then is refused as a loop. With the lock removed the test fails.
|
||||
|
||||
### WR-11: Undo is not idempotent under concurrent requests
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** d20f14f
|
||||
**Files:** `classes/undo.go`, `redirects_test.go`
|
||||
**Applied fix:**
|
||||
- `Undo` loads the revision `FOR UPDATE`.
|
||||
- `UndoBatch` locks `batch_id = ? AND undone_at IS NULL` rows `FOR UPDATE`.
|
||||
- The final mark is `WHERE id = ? AND undone_at IS NULL`. When `RowsAffected == 0` it returns `UndoRefusal{already_undone}`.
|
||||
|
||||
**Test:** `TestConcurrentUndo` checks that the second concurrent undo gets `already_undone`, the redirect is re-created once, and there is one undo history row. Before the fix the second undo hit a unique violation.
|
||||
|
||||
### WR-12: Public resolve and 404 report answer 500 for NUL or invalid UTF-8 paths
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 98fbeb6
|
||||
**Files:** `classes/resolver.go`, `classes/notfound.go`, `redirects_test.go`
|
||||
**Applied fix:**
|
||||
- **Resolve:** `parseTarget` answers `ErrBadURL` (422) when the decoded path, the raw query or a decoded query key or value is invalid UTF-8 or holds NUL. `safeText` is the new helper for this check.
|
||||
- **404 report:** `NotFoundPath` applies the same check to the logged address. The query stays escaped there, so it is safe text.
|
||||
- **Referer:** cleaned of NUL and invalid UTF-8.
|
||||
|
||||
**Test:** in `TestNotFoundLog`, `/%00`, `/%FF` and query variants get 422 from resolve. The report refuses the path cases with 422 and never answers 5xx.
|
||||
|
||||
### WR-13: The runtime cycle log grows without bound and is keyed by attacker-controlled URLs
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** c2ba33e
|
||||
**Files:** `classes/redirects.go`, `classes/redirect_graph_test.go`, `README.md`
|
||||
**Applied fix:** A mutex-guarded map replaces the global `sync.Map`:
|
||||
- it is keyed by the sorted rule IDs of the cycle, not the request URL;
|
||||
- each cycle is logged again at most once an hour;
|
||||
- the map is reset when it reaches 1024 keys.
|
||||
|
||||
**Test:** `TestCycleLogBounded` sends 5000 URLs through one cycle, which leaves one key, and checks that the map never exceeds the cap.
|
||||
|
||||
### WR-14: Export with a large `page` panics the handler
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 59c37a8
|
||||
**Files:** `classes/transfer.go`, `controllers/api/auth.go`, `transfer_test.go`
|
||||
**Applied fix:**
|
||||
- **Export:** a page past `Pages` is empty. The page is compared before multiplying, so an empty last page keeps the old semantics.
|
||||
- **List endpoints:** `listParams` caps `page` at `maxPage = 1_000_000`.
|
||||
|
||||
**Test:** `page=9223372036854777` on export and `page=MaxInt64` on a list both answer 200 with no rows.
|
||||
|
||||
### WR-15: Public throttle buckets key on the SSR server's IP, capping the whole site
|
||||
|
||||
**Status:** fixed (documentation only)
|
||||
**Commit:** cab1b9a
|
||||
**Files:** `README.md`, `routes.go` (comment)
|
||||
**Applied fix:** The README has a new paragraph, "Behind the frontend server". It says the SSR host must be in `http.trusted_proxies` and must forward `X-Forwarded-For`. Without that, `seo-resolve` and `seo-notfound` become caps for the whole site, 600 requests a minute each. The `Buckets` doc comment says the same.
|
||||
**Not done (needs a decision):** an authenticated resolve call that skips the throttle or gets a higher limit. It needs a new token or config key and a framework way for a surf `Bucket` to skip or raise its limit per request; today `FixedWindowLimiter` has no such hook. This is a design and framework change, not a plugin patch.
|
||||
|
||||
### WR-16: Social image values bypass validation on every non-admin write path
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** c8e0961
|
||||
**Files:** `classes/owner.go`, `classes/rules.go`, `classes/settings_write.go`, `mcp_test.go`
|
||||
**Applied fix:** The following now refuse a non-empty value that is not a path or an http(s) URL, with `golem15.seo::lang.image.url_invalid`:
|
||||
- `SetURLSEO` (`og_image`), which covers the API, MCP, batch `set_seo` and the records import;
|
||||
- `SaveRule` (`og_image`);
|
||||
- `SaveSettings` (`default_og_image`, only when that column is sent).
|
||||
|
||||
**Test:** `javascript:`, `data:`, `//` and a value with spaces get 422 on all three MCP routes.
|
||||
|
||||
### WR-17: Invalid redirect status, match or query values are silently coerced on API, MCP and import
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 24e6f38
|
||||
**Files:** `classes/batch.go`, `classes/transfer.go`, `controllers/api/redirects.go`, `mcp_test.go`, `batch_test.go`, `transfer_test.go`
|
||||
**Applied fix:** The new `RedirectFields.Problems()` refuses values outside {301, 302, 410}, {exact, pattern, regex} and {ignore, pass} with `api.value_invalid`. An empty match or query still means the default. It is checked before `Apply` in:
|
||||
- API create and update (422 `validation_failed`);
|
||||
- batch create and update redirect;
|
||||
- the CSV redirects import (the row is refused with `transfer.row_invalid`).
|
||||
|
||||
**Tests:** MCP `status: 308`, `match: Regex` and `query: keep` get 422; a batch with 307 gets 422; an import with `glob` or `keep` gives 2 refused rows.
|
||||
|
||||
### WR-18: `set_url_seo`, batch `set_seo` and the records import accept any locale
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** cff2d29
|
||||
**Files:** `classes/owner.go`, `controllers/api/seo.go` (jsonschema text), `lang/en/lang.yaml`, `lang/pl/lang.yaml`, `mcp_test.go`
|
||||
**Applied fix:** When a locale other than the matched one is given, `SetURLSEO` requires it to be one of the locales the record's descriptor describes (existing `recordLocales`). Otherwise it refuses with the new `api.locale_unknown` (en/pl). This covers the API, MCP, batch and the records import.
|
||||
**Test:** `xx` and `EN ` get 422 on `locale`; `pl` is written.
|
||||
|
||||
### WR-19: Entry generation overwrites `entry.URL`, which can swallow an address change and its 301
|
||||
|
||||
**Status:** fixed (requires human verification: address-change logic in the job)
|
||||
**Commit:** d8aa989
|
||||
**Files:** `classes/generate.go`, `seo_tab_test.go`, `README.md`
|
||||
**Applied fix:** `refreshEntry` (published records only) behaves as follows:
|
||||
- an empty URL is filled;
|
||||
- a URL that differs goes through `AddressChanged` (via auto), which creates the 301 and moves the URL;
|
||||
- it never overwrites the URL silently.
|
||||
|
||||
**Test:** `TestGenerateEntries` covers a slug changed by a direct DB write: the job now creates `/pages/one → /pages/one-renamed` and `/pl/pages/one → /pl/pages/one-renamed`. A third run adds nothing.
|
||||
|
||||
### WR-20: The social image preview accepts `/\host/...`
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 882d4ee
|
||||
**Files:** `assets/src/elements/image.ts`, `assets/dist/seo.js`, `classes/field_types.go`, `classes/urlref_test.go` (new), `tests/elements/image.test.ts` (new)
|
||||
**Applied fix:** `safeImageURL` (`/^\/(?![\/\\])/`) and `ValidURLRef` both refuse a path that starts with `/\`. `ValidURLRef` is also the canonical validator, so canonicals are stricter in the same way.
|
||||
**Tests:** a Go table test and a TS test.
|
||||
|
||||
### WR-21: The CSV export/import round trip changes cells that already start with an apostrophe followed by a formula character
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** ed2d919
|
||||
**Files:** `classes/csv.go`, `classes/csv_test.go`, `README.md`
|
||||
**Applied fix:**
|
||||
- A new `escapable(s)` helper checks for a formula character after any leading apostrophes.
|
||||
- `escapeCell` prefixes one more apostrophe to such values.
|
||||
- `unescapeCell` strips exactly one apostrophe, so it is the inverse of `escapeCell`.
|
||||
- The round-trip rows now include `'=x`, `''-)` and `'plain`.
|
||||
- The unit expectation for `unescapeCell("''=x")` changed to `'=x`.
|
||||
|
||||
This also resolves IN-08 (IN-B02), which is out of scope.
|
||||
|
||||
### WR-22: The 404 "Open/Resolved" filter and the redirect "Chains" filter disagree with the runtime matcher
|
||||
|
||||
**Status:** fixed (requires human verification: SQL matcher logic). Pattern and regex rules remain a documented gap.
|
||||
**Commits:** 7677d8d, ad419e9 (gofmt follow-up)
|
||||
**Files:** `models/redirect.go`, `models/notfound.go`, `controllers/api/notfound.go` (jsonschema), `redirects_test.go`
|
||||
**Applied fix:** A new `coversSQL(alias, addr)` is the SQL form of `exactMatches`:
|
||||
- the source path is compared with its `ignore_case` and `trailing_slash` options, and the query exactly when the source has one;
|
||||
- the indexed `source_key` from CR-03 serves as a prefilter.
|
||||
|
||||
`coveredSQL` (Open/Resolved, also MCP `list_not_found?status=`) and `chainSQL` (a path target that another rule's source names literally or an exact rule matches) both use it. The MCP `status` schema text now says that an address only a pattern or regex rule answers stays open in the filter but has `resolved: true` in its row.
|
||||
**Test:** `TestCoverAndChainFilters` covers:
|
||||
- `/missing/`, `/missing?utm=x`, `/case/` and `/q?id=1` are resolved;
|
||||
- `/MISSING`, `/other` and `/q?id=2` are open;
|
||||
- each row's badge agrees with the filter;
|
||||
- `/c1 → /c2/` is a chain next to `/c2`.
|
||||
|
||||
### WR-23: The URL rule address is uncapped but sits in a btree UNIQUE index
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 4ca26e9
|
||||
**Files:** `models/rule.go`, `mcp_test.go`
|
||||
**Applied fix:** `models.MaxRuleURL = 2048`, and `Rule.Problems` refuses longer addresses with `rules.url_invalid`. The check covers the admin form, the API, MCP, batches and imports.
|
||||
**Test:** a 3001-byte rule URL gets 422 on `url`.
|
||||
|
||||
### WR-24: The robots element's grammar drifts from the Go grammar, and an unparseable value is silently wiped on the next edit
|
||||
|
||||
**Status:** fixed
|
||||
**Commit:** 923d0f9
|
||||
**Files:** `assets/src/elements/robots.ts`, `labels.ts`, `styles.css`, `assets/dist/seo.js`, `assets/dist/seo.css`, `tests/elements/robots.test.ts`, `classes/robots_value_test.go`
|
||||
**Applied fix:**
|
||||
- **Dates:** the new `validDate` checks a real calendar date and handles leap years. `2027-02-30` is refused, as Go's `time.Parse` refuses it.
|
||||
- **Numbers:** an optional sign is accepted like `strconv.Atoi`, so `+5` reads as 5. The Go side was left unchanged so existing API callers keep working.
|
||||
- **Unparseable stored value:** the element keeps the raw value and shows a note (`robotsUnparsed`, en/pl) in place of the Inherit display. All controls are disabled, `commit` is blocked, and an explicit Reset button emits `''`.
|
||||
|
||||
**Tests:** the Feb-30, Apr-31 and day-00 dates are in the TS malformed list. A leap day and `+5` are accepted. An element test covers keep, disable and Reset. A Go test checks that `+5` normalises to `max-snippet:5`.
|
||||
|
||||
### WR-25: The element tests never exercise the host protocol; the tester and the image element have no tests
|
||||
|
||||
**Status:** fixed
|
||||
**Commits:** 3ecbd1f (fake host, SERP host-protocol tests, `robotstester.test.ts`), 882d4ee (`image.test.ts` `safeImageURL`), 5c2ad6a (transfer host tests, image element tests, README test gate)
|
||||
**Files:** `tests/elements/host.ts`, `serp.test.ts`, `robotstester.test.ts`, `transfer.test.ts`, `image.test.ts`, `README.md`
|
||||
**Applied fix:**
|
||||
- The fake widget host drives serp, robotstester and transfer. The tests cover queueing behind `busy`, `state=error` unblocking, the append-first ordering and dropped actions.
|
||||
- `golem15-seo-image` is tested for safe and unsafe previews, upload id matching (a foreign id is ignored), the error and the `imageFailed` fallback, Remove, and the disabled state.
|
||||
- The README test gate now includes `npm test`.
|
||||
|
||||
### Skipped Issues
|
||||
|
||||
None.
|
||||
|
||||
### Notes for the orchestrator
|
||||
|
||||
- **No schema edits to released migrations:** CR-03 adds the new migration `202610280001_add_golem15_seo_redirects_source_key`. v0.1.0's migrations are untouched.
|
||||
- **Framework half of CR-04:** the `WidgetField.vue` change belongs to the other fixer. The plugin now works with either wiring order.
|
||||
- **Needs human verification of logic:** WR-08, WR-09, WR-19 and WR-22 change matching, redirect-graph or SQL semantics. Syntax checks and DB tests pass, but the logic should be reviewed by a person.
|
||||
- **Behaviour now stricter for callers:**
|
||||
- unknown redirect status, match or query values are refused (WR-17);
|
||||
- unknown locales are refused (WR-18);
|
||||
- invalid social images are refused (WR-16);
|
||||
- `/\` paths are refused as image or canonical URLs (WR-20);
|
||||
- rule URLs over 2048 bytes are refused (WR-23).
|
||||
|
||||
These are worth a changelog line when the plugin is tagged.
|
||||
- **README sections updated:** redirects precedence, address changes, the loop lock, the runtime cycle log, "Behind the frontend server", entry generation, CSV escaping, and the Testing gate.
|
||||
|
||||
|
||||
_Fixed: 2026-10-27T14:45:18Z_
|
||||
_Fixer: Claude (msd-code-fixer)_
|
||||
_Iteration: 1_
|
||||
Reference in New Issue
Block a user