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”Outcome
Section titled “Outcome”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.
Confirmed Failure
Section titled “Confirmed Failure”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.
Safety Constraint
Section titled “Safety Constraint”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.
Behavior Contract
Section titled “Behavior Contract”Modified: save a preset
Section titled “Modified: save a preset”When an operator saves a Users preset, Strip must capture these non-empty fields:
searchstatuscreated_fromcreated_tocredits_mincredits_maxhas_charactershas_mediahas_reports
When an operator saves a Media preset, Strip must capture these non-empty fields:
searchtype_filterstatus_filtercreated_fromcreated_togeneration_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.
Modified: apply a preset
Section titled “Modified: apply a preset”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.
Removed: unrelated state
Section titled “Removed: unrelated state”Strip must not save or apply these values:
- authentication or bypass parameters such as
auth - pagination cursors
- preset form metadata such as
name,shared, andlist_type - fields that do not belong to the selected list type
Existing empty presets
Section titled “Existing empty presets”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.
Implementation Status
Section titled “Implementation Status”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-includeand initializes its disabled state onhtmx: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).
Error Behavior
Section titled “Error Behavior”- An unsupported
list_typemust 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.
Implementation Plan
Section titled “Implementation Plan”1. Add failing request-boundary tests
Section titled “1. Add failing request-boundary tests”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.
2. Define one allowlist source
Section titled “2. Define one allowlist source”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.
3. Submit live controls
Section titled “3. Submit live controls”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.
4. Sanitize apply data
Section titled “4. Sanitize apply data”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.
6. Regenerate and inspect Templ output
Section titled “6. Regenerate and inspect Templ output”Run templ generate from apps/strip/. Inspect the generated filters_templ.go diff. Do not hand-edit generated output.
7. Verify behavior
Section titled “7. Verify behavior”Run the focused regression first:
cd apps/stripgo test ./internal/app/handlers -run FilterPreset -count=1go test ./internal/app/templates/components/filters -run FilterPreset -count=1Run the Strip gate selected by tools/agent/cmd/agent-impact/main.go:
cd apps/stripmake lintmake run-testsRun the final impact check from the repository root:
go -C tools/agent run ./cmd/agent-impact -changed -strictInspect the diff after generation and after every mutating gate.
Acceptance Criteria
Section titled “Acceptance Criteria”- Apply a Users status filter that changes the displayed rows. Save it, clear it, then select it. The same filtered result set must return.
- Repeat the flow with a Users date, numeric, boolean, and search filter.
- Repeat the flow with a Media filter because the component is shared.
- Leave every allowed field empty. The UI must disable save, and a crafted request must receive HTTP 400 without a Sirloin call.
- Save a preset while the URL contains
authandcursor. Neither key may reach Sirloin, rendered HTML, or the apply URL. - Select a legacy preset containing allowed and unknown keys. Strip must apply only the allowed keys.
- Load an invalid owned preset. It must be disabled, show
No valid filters saved, and retain its delete action. - Load an invalid shared preset as a non-owner. It must not appear in rendered HTML.
- The rendered-script harness must produce the expected full-navigation URL for Users and Media.
- The focused regressions, full Strip lint, race-enabled Strip tests, and impact check must pass.
Dependencies and Evidence Limits
Section titled “Dependencies and Evidence Limits”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.