Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/core-web-vitals-reach-analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"hephaestus": minor
---

Instances that run PostHog now receive Core Web Vitals — largest contentful paint, cumulative layout
shift, first contentful paint and interaction to next paint — for the web application, so a slow
page is visible in analytics rather than only in a complaint. The application never measured them
before. Nothing is captured without the analytics consent that already gates every other event, and
network request timing stays off.
7 changes: 7 additions & 0 deletions .changeset/current-web-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"hephaestus": patch
---

Updates the frameworks the web application is built from. Error reports still infer nothing about
who reported them, and now say category by category what they may carry. No action is needed to
upgrade.
9 changes: 9 additions & 0 deletions .changeset/sorted-columns-say-so.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"hephaestus": patch
---

The sortable columns in the workspace members and achievements admin tables now say which column is
sorted and in which direction — the arrow points, and a screen reader announces it. Both showed the
same static icon whatever the sort was. Their "Columns" menus no longer offer the column of row
actions, which was never meaningful to hide, and while a search is active both tables now count
"Showing 5 of 12" against the rows that matched rather than against every row.
212 changes: 101 additions & 111 deletions bun.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"@mermaid-js/layout-elk": "0.2.1",
"clsx": "2.1.1",
"prism-react-renderer": "2.4.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"react": "19.2.8",
"react-dom": "19.2.8",
"@docusaurus/plugin-content-docs": "3.10.1",
"@docusaurus/plugin-content-pages": "3.10.1",
"@docusaurus/plugin-css-cascade-layers": "3.10.1",
Expand All @@ -38,8 +38,8 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/types": "3.10.1",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"markdownlint-cli2": "0.22.1",
"raw-loader": "4.0.2",
"typescript": "7.0.2"
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"commander": "14.0.3",
"fast-xml-parser": "^5.11.0",
"fast-xml-validator": "1.4.2",
"graphql": "16.14.0",
"graphql": "17.0.2",
"husky": "9.1.7",
"jsdom": "29.1.1",
"jsonc-parser": "3.3.1",
Expand Down Expand Up @@ -155,8 +155,7 @@
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"serialize-javascript": "7.0.5",
"js-yaml": "4.3.1",
"@opentelemetry/core": "2.8.0"
"js-yaml": "4.3.1"
},
"trustedDependencies": [
"@swc/core",
Expand Down
30 changes: 30 additions & 0 deletions scripts/check-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ if (!isRecord(packages)) throw new Error("bun.lock must contain packages");
const packageValues = Object.values(packages).filter((value): value is unknown[] =>
Array.isArray(value),
);
/**
* `bunfig.toml#publicHoistPattern` lifts a package to the root `node_modules` so the whole repo
* shares one copy. For React and its types a second copy is two incompatible `JSX.Element`s, which
* surfaces as a type error deep inside an unrelated library. Hoisting can only lift one, so the
* lockfile resolving two is the moment that guarantee is gone — and nothing else notices.
*/
const bunfig: unknown = Bun.TOML.parse(readFileSync("bunfig.toml", "utf8"));
const publicHoistPattern =
isRecord(bunfig) && isRecord(bunfig.install) ? bunfig.install.publicHoistPattern : undefined;
if (!Array.isArray(publicHoistPattern) || publicHoistPattern.length === 0) {
throw new Error("bunfig.toml must declare install.publicHoistPattern");
}
for (const pattern of publicHoistPattern) {
// A glob cannot be matched against a resolution prefix, so it is left to Bun.
if (typeof pattern !== "string" || /[*?[\]]/.test(pattern)) continue;
const resolutions = new Set(
packageValues
.map(([resolution]) => resolution)
.filter((resolution): resolution is string => typeof resolution === "string")
.filter((resolution) => resolution.startsWith(`${pattern}@`)),
);
if (resolutions.size === 0)
throw new Error(`Publicly hoisted ${pattern} matches no lockfile package`);
if (resolutions.size > 1) {
throw new Error(
`Publicly hoisted ${pattern} resolves to ${resolutions.size} versions, so the workspaces cannot share one copy: ${[...resolutions].join(", ")}`,
);
}
}

for (const [dependency, expected] of overrideEntries) {
const resolutions = packageValues
.map(([resolution]) => resolution)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32768,7 +32768,7 @@ type Group implements GroupInterface & Todoable {
sort: ComplianceViolationSort = SEVERITY_LEVEL_DESC

"""Filters applied when retrieving compliance violations."""
filters: ComplianceViolationInput = {}
filters: ComplianceViolationInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand Down Expand Up @@ -33237,7 +33237,7 @@ type Group implements GroupInterface & Todoable {
"""
projectComplianceRequirementsStatus(
"""Filters applied when retrieving compliance requirement statuses."""
filters: GroupProjectRequirementComplianceStatusInput = {}
filters: GroupProjectRequirementComplianceStatusInput = { }

"""Field used to sort compliance requirement statuses."""
orderBy: ProjectComplianceRequirementStatusOrderBy
Expand All @@ -33262,7 +33262,7 @@ type Group implements GroupInterface & Todoable {
"""
projectComplianceStandardsAdherence(
"""Filters applied when retrieving compliance standards adherence."""
filters: ComplianceStandardsAdherenceInput = {}
filters: ComplianceStandardsAdherenceInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand All @@ -33284,7 +33284,7 @@ type Group implements GroupInterface & Todoable {
"""
projectComplianceViolations(
"""Filters applied when retrieving compliance violations."""
filters: ProjectComplianceViolationFilterInput = {}
filters: ProjectComplianceViolationFilterInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand Down Expand Up @@ -60592,7 +60592,7 @@ type Project implements ProjectInterface & Todoable {
"""
Filters applied when retrieving compliance control statuses for the project.
"""
filters: ProjectComplianceControlStatusInput = {}
filters: ProjectComplianceControlStatusInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand Down Expand Up @@ -60634,7 +60634,7 @@ type Project implements ProjectInterface & Todoable {
"""
complianceRequirementStatuses(
"""Filters applied when retrieving compliance requirement statuses."""
filters: ProjectRequirementComplianceStatusInput = {}
filters: ProjectRequirementComplianceStatusInput = { }

"""Field used to sort compliance requirement statuses."""
orderBy: ProjectComplianceRequirementStatusOrderBy
Expand All @@ -60657,7 +60657,7 @@ type Project implements ProjectInterface & Todoable {
"""Compliance standards adherence for the project."""
complianceStandardsAdherence(
"""Filters applied when retrieving compliance standards adherence."""
filters: ComplianceStandardsProjectAdherenceInput = {}
filters: ComplianceStandardsProjectAdherenceInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand Down Expand Up @@ -62146,7 +62146,7 @@ type Project implements ProjectInterface & Todoable {
sort: ComplianceViolationSort = SEVERITY_LEVEL_DESC

"""Filters applied when retrieving compliance violations."""
filters: ComplianceViolationProjectInput = {}
filters: ComplianceViolationProjectInput = { }

"""Returns the elements in the list that come after the specified cursor."""
after: String
Expand Down
1 change: 0 additions & 1 deletion webapp/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const config: StorybookConfig = {
"ai",
"posthog-js/react",
"uuid",
"web-vitals",
];
return viteConfig;
},
Expand Down
14 changes: 8 additions & 6 deletions webapp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,14 @@ suppressed at the call site with their reason.
`forwardRef` are not imports you may take from `react` — the import line says so and why.

**A `useMemo` that survives is load-bearing, and deleting it breaks something that still type-checks.**
There are only two shapes that earn the suppression, and each names its own on the line above the
There are only three shapes that earn the suppression, and each names its own on the line above the
import: the value is a **dependency of an effect**, so its identity is what decides whether the effect
re-runs; or the component is **opted out of the compiler** by a library it uses, so nothing memoises
for it. `useReactTable` is the second case — `react/incompatible-library` names the same fact — and
TanStack Table rebuilds its column model, and every row model downstream, whenever `columns` changes
identity. Anything else is the memo the compiler exists to remove.
re-runs; the component is **opted out of the compiler** by a library it uses, so nothing memoises for
it — `react/incompatible-library` names that case where it can; or a **library keys a cache on the
value's identity**, which the compiler memoises as an optimisation rather than promises. TanStack
Table is the third: `useTable` compares `options.columns` and `options.data` with `!==` and rebuilds
the column model, and every row model downstream, when either changes — so a table here memoises
them by hand. Anything else is the memo the compiler exists to remove.

## The time of day

Expand Down Expand Up @@ -232,7 +234,7 @@ a routing table on the front page; its `RUBRIC.md` is the grading instrument for
## Routing

Declare routes with `createFileRoute`. Keep loaders side-effect free and prefer
`context.queryClient.ensureQueryData(...)` with the generated options. Put shared data (query client,
`context.queryClient.query(...)` with the generated options. Put shared data (query client,
auth) on the router context. Never hand-edit `routeTree.gen.ts` — there is no `tsr` CLI here; the
TanStack Router Vite plugin regenerates it when the dev server runs.

Expand Down
47 changes: 23 additions & 24 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,40 @@
"generate:api": "bun --bun openapi-ts"
},
"dependencies": {
"@ai-sdk/react": "3.0.186",
"@ai-sdk/react": "4.0.82",
"@base-ui/react": "1.7.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0",
"@primer/octicons-react": "19.25.0",
"@sentry/react": "10.53.1",
"@tanstack/react-query": "5.100.10",
"@tanstack/react-query-devtools": "5.100.10",
"@tanstack/react-router": "1.170.4",
"@tanstack/react-router-devtools": "1.167.0",
"@tanstack/react-table": "8.21.3",
"@xyflow/react": "^12.10.2",
"ai": "6.0.184",
"@primer/octicons-react": "19.33.0",
"@sentry/react": "10.71.0",
"@tanstack/react-query": "5.102.5",
"@tanstack/react-query-devtools": "5.102.5",
"@tanstack/react-router": "1.170.32",
"@tanstack/react-router-devtools": "1.167.1",
"@tanstack/react-table": "9.1.2",
"@xyflow/react": "^12.11.5",
"ai": "7.0.79",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"date-fns": "4.4.0",
"fast-deep-equal": "3.1.3",
"lucide-react": "1.16.0",
"motion": "12.38.0",
"posthog-js": "1.373.5",
"react": "19.2.6",
"lucide-react": "1.34.0",
"motion": "13.1.1",
"posthog-js": "1.419.4",
"react": "19.2.8",
"react-day-picker": "10.0.1",
"react-dom": "19.2.6",
"sonner": "2.0.7",
"streamdown": "2.5.0",
"react-dom": "19.2.8",
"sonner": "2.0.8",
"streamdown": "2.6.0",
"tailwind-merge": "3.6.0",
"use-sync-external-store": "1.6.0",
"usehooks-ts": "3.1.1",
"uuid": "14.0.0",
"web-vitals": "5.2.0",
"uuid": "14.0.2",
"zod": "4.4.3",
"zustand": "5.0.13"
"zustand": "5.0.15"
},
"devDependencies": {
"@chromatic-com/storybook": "5.2.1",
Expand All @@ -82,14 +81,14 @@
"@storybook/react-vite": "10.4.0",
"@tailwindcss/typography": "0.5.19",
"@tailwindcss/vite": "4.3.0",
"@tanstack/router-plugin": "1.168.6",
"@tanstack/router-plugin": "1.168.35",
"@testing-library/dom": "10.4.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/hast": "3.0.5",
"@types/node": "24.12.4",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"@vitejs/plugin-react": "6.1.0",
"@vitest/browser": "4.1.11",
"@vitest/browser-playwright": "4.1.11",
Expand Down
21 changes: 19 additions & 2 deletions webapp/src/components/admin/AdminAchievementsTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn } from "storybook/test";
import { expect, fn, userEvent } from "storybook/test";

import { AdminAchievementsTable } from "./AdminAchievementsTable";
import type { ExtendedUserTeams } from "./types";
Expand Down Expand Up @@ -36,7 +36,24 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
export const Default: Story = {
play: async ({ canvas }) => {
// Re-queried each time: sorting re-renders the header, so a held reference goes stale.
const nameHeader = () => canvas.getByRole("columnheader", { name: "Name" });
const sortByName = () => canvas.getByRole("button", { name: "Name" });

await expect(nameHeader()).toHaveAttribute("aria-sort", "none");
await userEvent.click(sortByName());
await expect(nameHeader()).toHaveAttribute("aria-sort", "ascending");
await userEvent.click(sortByName());
await expect(nameHeader()).toHaveAttribute("aria-sort", "descending");

// A column that cannot sort claims no sort state at all.
await expect(canvas.getByRole("columnheader", { name: "Actions" })).not.toHaveAttribute(
"aria-sort",
);
},
};

export const Loading: Story = {
args: { users: [], isLoading: true },
Expand Down
Loading
Loading