From 9857f99f7aba164cc2747ae5ca79ad081a6f3c8d Mon Sep 17 00:00:00 2001 From: Chris Dyson Date: Wed, 23 Sep 2026 09:40:34 +1200 Subject: [PATCH] Make the support matrix filter filter, and search operation names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The support matrix filter updated its count and left every row on screen. The script toggled the `hidden` class, and each row's own `flex` and `sm:grid` won the cascade. Rows now take the `hidden` attribute, which preflight backs with `!important`. With the filter working, the rest of the page is built around using it: - Clearing it: an × in the field, Esc, a "Clear filters" link beside the count, and a second one in the empty state. - Operation names: `PutBucketCors`, `put bucket` or `tag` finds the services that list the operation, with the matching operations and their status under the row. A match has to start at a word in the CamelCase name, so "tag" finds TagResource and not CreateStage. Partial and unsupported operations sort first, ahead of "+N more", since the question is usually whether a call will work. - Looser name matching: every word has to match, against the name and a form with punctuation removed, so `cloud logs`, `cloudwatch-logs` and `CloudWatchLogs` all find CloudWatch Logs. The matched part of the name is highlighted. - All / Has gaps / Complete toggles, whose counts follow the text filter. Pressing the active one again returns to All. - The query, toggle and sort live in the URL (`?q=bucket&show=gaps&sort=gaps`), so a filtered view can be linked and survives the back button from a service page. The operation names are ~1,500 strings most visits never use, so they are served from `/support/operations.json`, built from the same support manifest as the page, and fetched on intent: the first hover or focus on the filter, or on load when the URL carries a query. The page is 21.4 KB gzipped (30.4 KB with the names inline) and the JSON is 8.2 KB. Service names match immediately; while the names are in flight the count says so and the empty state waits. A failed fetch leaves service-name matching working and is retried on the next focus. Checked with `npm run check`, `npm run build` and `npm test`, and in the browser against the built site at desktop and phone widths: filtering, both clears, Esc, the toggles, sorting, URL restore, no request on page load, one on focus and none on refocus. The failed-fetch path was not exercised. --- src/pages/support.astro | 441 +++++++++++++++++++++++---- src/pages/support/operations.json.ts | 20 ++ 2 files changed, 398 insertions(+), 63 deletions(-) create mode 100644 src/pages/support/operations.json.ts diff --git a/src/pages/support.astro b/src/pages/support.astro index c8c428b..a5c0590 100644 --- a/src/pages/support.astro +++ b/src/pages/support.astro @@ -4,7 +4,7 @@ import Callout from "../components/Callout.astro"; import SearchPromo from "../components/SearchPromo.astro"; import ServiceIcon from "../components/ServiceIcon.astro"; import Chip from "../components/Chip.astro"; -import { Search, ShieldCheck } from "@lucide/astro"; +import { Search, ShieldCheck, X } from "@lucide/astro"; import { getServiceSupport } from "../lib/generated-content"; const support = await getServiceSupport(); @@ -32,6 +32,12 @@ const services = support.services const completeCount = services.filter((service) => service.gap === 0).length; const totalGap = services.reduce((sum, service) => sum + service.gap, 0); + +const showOptions: Array<{ value: "all" | "gaps" | "complete"; label: string; count: number }> = [ + { value: "all", label: "All", count: services.length }, + { value: "gaps", label: "Has gaps", count: services.length - completeCount }, + { value: "complete", label: "Complete", count: completeCount }, +]; --- @@ -77,43 +83,76 @@ const totalGap = services.reduce((sum, service) => sum + service.gap, 0); -
+
{/* A placeholder is not a label: it disappears the moment anything is typed and is not what a screen reader reads for the field. The visible design has no room for a caption, so the name is carried off-screen. */} - + -
- + + +
+
+ {/* Toggle buttons rather than radios: each is one press, and the counts on them + follow the text filter, so they say what pressing one would leave. */} +
+ {showOptions.map((option) => ( + + ))} +
+ +
{/* The count labels the table, so it keeps a `group` gap to it and takes the full `block` gap from the filters above — the three used to stack at 12px - each and read as one undifferentiated pile. */} -

- Showing all {services.length} services -

+ each and read as one undifferentiated pile. The reset sits beside it, outside + the live region, so it is not read out on every keystroke. */} +
+

+ Showing all {services.length} services +

+ +
{/* The column strip is a visual key for sighted readers only: nothing associates @@ -133,44 +172,66 @@ const totalGap = services.reduce((sum, service) => sum + service.gap, 0); {/* One line per service at every width. The meter is the only thing that drops on a phone: keeping it stacked each row onto two lines, which made a 50-row table half as long again as the one it replaced. The counts beside it carry - the same fact exactly. */} - {services.map((service) => ( - ))}
- +
@@ -185,14 +246,96 @@ const totalGap = services.reduce((sum, service) => sum + service.gap, 0); diff --git a/src/pages/support/operations.json.ts b/src/pages/support/operations.json.ts new file mode 100644 index 0000000..d119e8b --- /dev/null +++ b/src/pages/support/operations.json.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro"; +import { getServiceSupport } from "../../lib/generated-content"; + +// https://overcast.sh/support/operations.json — every listed operation and its status, +// keyed by service id. The support matrix filter fetches it the first time someone goes +// to use the filter, so the ~1,500 names are not part of every visit to the page. +type Operation = { operation: string; status: string }; + +export const GET: APIRoute = async () => { + const support = await getServiceSupport(); + const index = Object.fromEntries( + support.services.map((service) => [ + service.service, + (service.operations as Operation[]).map((op) => [op.operation, op.status]), + ]), + ); + return new Response(JSON.stringify(index), { + headers: { "Content-Type": "application/json; charset=utf-8" }, + }); +};