Skip to content

Strip Filter Presets FOXY-1029 Specification and Repair Plan

Strip Filter Presets FOXY-1029 Specification and Repair Plan

Section titled “Strip Filter Presets FOXY-1029 Specification and Repair Plan”

Strip must save the active list filters in a named preset. Selecting that preset must display the matching list results.

FOXY-1029 covers the Users failure and the shared Media preset path. The repair stays within Strip. It does not change the Sirloin protobuf or database schema.

The Users filter form sends an HTMX request to /users and swaps only #users-table-container (apps/strip/internal/app/templates/pages/users.templ:107-111). The request can contain status=2 while the browser remains at /users.

Before this repair, the preset form built filter_json only from window.location.search. It therefore saved {} when the active filter existed only in the HTMX request.

The preset apply function converted that empty object back to /users. The unfiltered list remained visible after the navigation.

The local reproduction produced this state:

{
"htmxRequest": "/users?status=2",
"browserLocation": "/users",
"savedFilterJSON": "{}",
"appliedURL": "/users"
}

The original focused handler tests submitted a prepared filter_json value. They did not exercise browser state capture.

The pre-repair form serialized every URL parameter. A preset could therefore persist auth, cursor, or another unrelated parameter.

Sirloin checks that filter_json is valid JSON, but treats its contents as opaque (apps/sirloin/internal/app/services/strip/stripfilterpresets.go:49-75). Strip must allowlist preset fields before it sends or renders the JSON.

When an operator saves a Users preset, Strip must capture these non-empty fields:

  • search
  • status
  • created_from
  • created_to
  • credits_min
  • credits_max
  • has_characters
  • has_media
  • has_reports

When an operator saves a Media preset, Strip must capture these non-empty fields:

  • search
  • type_filter
  • status_filter
  • created_from
  • created_to
  • generation_mode_filter

The saved JSON must use string keys and string values. Empty fields must be absent.

At least one allowed field must contain a value. The save control must be disabled when every allowed field is empty. Strip must also reject a crafted or stale empty submission.

Selecting a preset must navigate to the current list path with the stored, allowed fields as its query string. The normal list handler remains the acceptance oracle for matching results.

When legacy JSON contains allowed and unknown fields, Strip must apply the allowed fields and discard the unknown fields. Unknown fields must not appear in the rendered page or destination URL.

Preset application must remain a full navigation. This keeps the result list, controls, query URL, refresh behavior, and browser history synchronized.

Strip must not save or apply these values:

  • authentication or bypass parameters such as auth
  • pagination cursors
  • preset form metadata such as name, shared, and list_type
  • fields that do not belong to the selected list type

The repair cannot reconstruct criteria that were previously saved as {}. Operators must recreate those presets.

Strip must treat empty, malformed, or unknown-only legacy JSON as an invalid preset. An owner must see the preset disabled with the message No valid filters saved and retain its delete action. Strip must hide an invalid shared preset from operators who do not own it.

Strip must not update, migrate, or delete legacy rows while listing them.

The change owns the Users and Media preset paths because both use FilterPresetList. It changes Strip request assembly and rendered preset data.

The change does not modify filtering semantics, result queries, sharing rules, preset ownership, persistence, protobufs, or migrations. It does not preserve pagination position, sort order, or page size in a preset.

Completion does not require an authenticated browser test. Automated request, rendering, and JavaScript boundary tests are the acceptance substitute.

Implemented in Strip on 2026-08-28.

  • One server allowlist defines Users and Media preset fields (apps/strip/internal/app/handlers/filterpresets.go:22-48).
  • Save requests ignore client JSON and build deterministic JSON from allowed form values (apps/strip/internal/app/handlers/filterpresets.go:50-72).
  • Stored presets are sanitized before rendering (apps/strip/internal/app/handlers/filterpresets.go:89-146).
  • The lazy partial submits live controls through hx-include and initializes its disabled state on htmx:load (apps/strip/internal/app/templates/components/filters/filters.templ:535-644).
  • Preset application remains a full navigation (apps/strip/internal/app/templates/components/filters/filters.templ:647-660).
  • The Users Clear action targets only the Users filter form, so it cannot erase or submit preset metadata (apps/strip/internal/app/templates/pages/users.templ:107-206).
  • An unsupported list_type must return HTTP 400 before Strip calls Sirloin.
  • A save request with no non-empty allowed field must return HTTP 400 before Strip calls Sirloin.
  • JSON construction from allowed string fields must be deterministic.
  • A Sirloin save failure must keep the existing HTTP 400 response.
  • A Sirloin list failure must keep the existing HTTP 500 response.
  • Malformed legacy JSON must never expose its original contents in rendered HTML.
  • An invalid owned preset must render disabled with its delete action.
  • An invalid shared preset must not render for a non-owner.

Update apps/strip/internal/app/handlers/filterpresets_test.go.

Add a Users save case that posts live filter fields without filter_json. Assert that the Sirloin request contains each allowed non-empty Users field.

Post auth, cursor, filter_json, Media-only fields, and preset metadata in the same case. Assert that none enters the saved JSON.

Add the equivalent Media case. Add unsupported-list and empty-preset rejection cases.

Add legacy-list cases for these states:

  • allowed fields plus unknown fields
  • empty JSON
  • malformed JSON
  • unknown-only JSON
  • owned invalid preset
  • shared invalid preset owned by another operator

These tests must fail against the current handler because it reads only filter_json.

Update apps/strip/internal/app/handlers/filterpresets.go.

Define the allowed field names by list_type. Use the same list to build the HTMX include selector, detect an empty preset, construct save JSON, and sanitize stored JSON.

Do not duplicate field lists between the handler and template.

Update apps/strip/internal/app/templates/components/filters/filters.templ.

Replace the window.location.search serializer with an hx-include selector supplied by the handler. Keep name, shared, and list_type in the preset form.

The handler must ignore any client-supplied filter_json. It must build JSON from allowed included fields.

Use the same server-supplied selector to disable the save button while all allowed controls are empty. Initialize this state after the lazy HTMX preset swap. Update it on relevant input and change events.

Before rendering FilterPresetList, parse each preset JSON object and retain only fields allowed for its list type. Mark empty, malformed, and unknown-only JSON as invalid.

Pass only sanitized JSON to applyPreset. Apply valid fields even when the stored object also contains unknown fields.

Pass ownership and validity to the component. Render an invalid owned preset as disabled with its delete action. Omit an invalid preset owned by another operator. Do not write the sanitized value back to Sirloin.

5. Add rendering and JavaScript boundary tests

Section titled “5. Add rendering and JavaScript boundary tests”

Add focused tests under apps/strip/internal/app/templates/components/filters/.

Render valid, invalid-owned, and invalid-foreign presets. Assert the visible, disabled, hidden, and delete-action states. Assert that unsafe stored values never occur in rendered HTML.

Execute the actual rendered applyPreset function and invocation with Node’s built-in vm API. Assert the generated Users and Media destination URLs. The harness must execute emitted code instead of duplicating its logic.

Run templ generate from apps/strip/. Inspect the generated filters_templ.go diff. Do not hand-edit generated output.

Run the focused regression first:

Terminal window
cd apps/strip
go test ./internal/app/handlers -run FilterPreset -count=1
go test ./internal/app/templates/components/filters -run FilterPreset -count=1

Run the Strip gate selected by tools/agent/cmd/agent-impact/main.go:

Terminal window
cd apps/strip
make lint
make run-tests

Run the final impact check from the repository root:

Terminal window
go -C tools/agent run ./cmd/agent-impact -changed -strict

Inspect the diff after generation and after every mutating gate.

  1. Apply a Users status filter that changes the displayed rows. Save it, clear it, then select it. The same filtered result set must return.
  2. Repeat the flow with a Users date, numeric, boolean, and search filter.
  3. Repeat the flow with a Media filter because the component is shared.
  4. Leave every allowed field empty. The UI must disable save, and a crafted request must receive HTTP 400 without a Sirloin call.
  5. Save a preset while the URL contains auth and cursor. Neither key may reach Sirloin, rendered HTML, or the apply URL.
  6. Select a legacy preset containing allowed and unknown keys. Strip must apply only the allowed keys.
  7. Load an invalid owned preset. It must be disabled, show No valid filters saved, and retain its delete action.
  8. Load an invalid shared preset as a non-owner. It must not appear in rendered HTML.
  9. The rendered-script harness must produce the expected full-navigation URL for Users and Media.
  10. The focused regressions, full Strip lint, race-enabled Strip tests, and impact check must pass.

The implementation requires no new package, service, migration, or contract generation. The JavaScript harness uses the installed Node runtime and its built-in vm API.

The issue recording and Slack thread were not available in the local checkout. The reproduction covers the client state transition shown by the current source. The agreed acceptance boundary does not include an authenticated browser check, so the complete Clerk-plus-HTMX runtime remains a residual risk.